{
  "openapi": "3.1.0",
  "info": {
    "title": "TonePerfect Pronunciation Assessment API",
    "version": "0.1.0",
    "summary": "Score a short recording against its reference text: sentence, word and sound grades, with Mandarin initial, final and tone per syllable.",
    "description": "Send a recording (WAV, MP3, M4A, Ogg or WebM, 0.15–30 s, up to 10 MB) and the text it should contain. The response has 0–100 display grades for the recording, every word and every sound, expected vs heard values, timestamps and correction decisions. 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).\n\nBilling: one credit per started 15 seconds of audio for an assessable result; failed and unassessable requests cost nothing. Every new account gets 50 free credits that work without a plan; after that an active monthly plan is required (from $10/month for 2,500 credits). Every assessment needs an `Idempotency-Key`; retrying with the same key and identical fields returns the stored result without charging again.\n\nDocumentation: https://api.toneperfect.app/docs · Quickstart: https://api.toneperfect.app/quickstart/ · Pricing: https://api.toneperfect.app/pricing/",
    "contact": {
      "name": "TonePerfect",
      "email": "nik@toneperfect.app",
      "url": "https://api.toneperfect.app/"
    }
  },
  "externalDocs": {
    "description": "API reference",
    "url": "https://api.toneperfect.app/docs"
  },
  "servers": [
    {
      "url": "https://api.toneperfect.app",
      "description": "Production. Requests pass a proxy with a 26-second timeout."
    }
  ],
  "tags": [
    {
      "name": "Assessment"
    },
    {
      "name": "Capabilities"
    },
    {
      "name": "Utilities"
    }
  ],
  "paths": {
    "/v1/assess": {
      "post": {
        "tags": [
          "Assessment"
        ],
        "operationId": "assessPronunciation",
        "summary": "Assess a short recording",
        "description": "Multipart upload of the recording plus its reference. Provide `text`, or (Mandarin only) `pinyin`, or both with one pinyin syllable per character. 60 requests per minute per account; assessments in progress at once per plan are listed in `GET /v1/terms` (`limits.concurrent_requests_by_plan`). A `tp_test_` key returns a fixed example result (`test: true`), never scores audio and never spends credits.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyHeader": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Unique per recording attempt. Retry with the same key and identical fields when the outcome is unknown; use a new key for a new attempt. Results are kept for 24 hours.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_.:-]{8,128}$",
              "examples": [
                "lesson-42-attempt-1"
              ]
            }
          },
          {
            "name": "TonePerfect-Response",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "compact"
              ]
            },
            "description": "Request the versioned toneperfect.compact.v1 shape; default engine-oriented shape remains available."
          },
          {
            "name": "TonePerfect-Test-Outcome",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "success",
                "unassessable",
                "invalid_assessment",
                "engine_unavailable",
                "subscription_required",
                "insufficient_credits",
                "account_busy",
                "rate_limit"
              ]
            },
            "description": "Test keys (`tp_test_…`) only: the outcome the fixed example response rehearses. Ignored by live keys."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "audio"
                ],
                "properties": {
                  "audio": {
                    "type": "string",
                    "format": "binary",
                    "description": "The recording: WAV, MP3, M4A, Ogg or WebM, 0.15–30 seconds, at most 10 MB."
                  },
                  "text": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "The expected words. Required unless `pinyin` is given. Mandarin: up to 100 characters, numbers and foreign words written in Chinese. Other languages: up to 40 words in the language’s own script (Latin; Cyrillic for ru-RU; Hangul with normal spacing for ko-KR; kana and/or kanji for ja-JP, split into bunsetsu-like units, kanji read with a dictionary), numbers written as words.",
                    "examples": [
                      "你好"
                    ]
                  },
                  "language": {
                    "type": "string",
                    "enum": [
                      "zh-CN",
                      "en-US",
                      "fr-FR",
                      "es-419",
                      "de-DE",
                      "it-IT",
                      "pt-BR",
                      "ru-RU",
                      "ko-KR",
                      "ja-JP"
                    ],
                    "default": "zh-CN",
                    "description": "Locale. Every pack is labelled experimental; ru-RU, ko-KR and ja-JP were added in October 2026. See GET /v1/capabilities for live availability."
                  },
                  "task": {
                    "type": "string",
                    "enum": [
                      "general",
                      "connected_speech",
                      "isolated_word"
                    ],
                    "default": "general",
                    "description": "Practice context. Mandarin component correction decisions from the human-judgment model apply with `connected_speech`."
                  },
                  "pinyin": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "Mandarin only: numbered-tone pinyin, space-separated, one syllable per character (up to 100), e.g. `ni3 hao3`; `v` for ü; 5 or 0 for the neutral tone.",
                    "examples": [
                      "ni3 hao3"
                    ]
                  }
                }
              },
              "encoding": {
                "audio": {
                  "contentType": "audio/wav, audio/mpeg, audio/mp4, audio/ogg, audio/webm, application/octet-stream"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Assessment completed. Check `assessable`: when it is false the recording could not be assessed (`reason` says why) and nothing was charged.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Idempotent-Replayed": {
                "description": "`true` when the stored result for this Idempotency-Key was returned.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                }
              },
              "X-TonePerfect-Test": {
                "description": "`true` on a fixed example response from a test key.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                }
              },
              "X-TonePerfect-Credits-Charged": {
                "description": "Credits this request used.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/Assessment"
                    },
                    {
                      "$ref": "#/components/schemas/CompactAssessment"
                    }
                  ]
                },
                "examples": {
                  "compact": {
                    "summary": "Documented compact shape example; not an accuracy benchmark",
                    "value": {
                      "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
                      }
                    }
                  },
                  "mandarin": {
                    "summary": "Real response for 你好 (trimmed to documented fields)",
                    "value": {
                      "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
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Possible `error.code` values:\n- `invalid_multipart`: The body is not readable multipart/form-data. Send multipart fields audio, text and language; let your HTTP client set the boundary.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized. Possible `error.code` values:\n- `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.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Payment required. Possible `error.code` values:\n- `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.\n- `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.\n- `payment_balance_due`: A refunded or disputed purchase left a balance to settle. The next purchase settles it first.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "408": {
            "description": "Upload timeout. Possible `error.code` values:\n- `upload_timeout`: The upload took longer than 30 seconds. Nothing was charged. Retry with the same key.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency conflict. Possible `error.code` values:\n- `idempotency_conflict`: This Idempotency-Key was used with different audio or fields. Use a new key for a new attempt.\n- `request_pending`: The assessment for this key is still running. Retry the same key shortly.\n- `idempotency_expired`: The stored result for this key is older than 24 hours. Use a new key. An expired key never charges again.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Payload too large. Possible `error.code` values:\n- `upload_too_large`: The upload is over 10 MB. Send a shorter or compressed recording.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable request. Possible `error.code` values:\n- `invalid_idempotency_key`: The Idempotency-Key header is missing or malformed. Send 8–128 letters, digits, dots, dashes, underscores or colons.\n- `missing_audio`: No file in the audio field. Attach the recording as a file part named audio.\n- `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).\n- `invalid_task`: task is not one of the accepted values. Use general, connected_speech or isolated_word.\n- `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.\n- `invalid_input`: text, pinyin or language is not a string. Send them as plain form fields.\n- `unsupported_language`: The locale is not available. Choose an available locale from GET /v1/capabilities.\n- `invalid_audio`: The file could not be decoded, or is shorter than 0.15 s or longer than 30 s. Send WAV, MP3, M4A, Ogg or WebM between 0.15 and 30 seconds.\n- `invalid_assessment` (finalized: carries `id` and `billing`; retry with a new key): 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.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests. Possible `error.code` values:\n- `rate_limit`: More than 60 assessment requests in a minute for this account. Slow down and retry the same key after a short delay.\n- `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.\n- `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.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Retry-After": {
                "description": "Seconds to wait (sent with `account_busy` and `rate_limit`).",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Service unavailable. Possible `error.code` values:\n- `gateway_busy`: The upload gateway is at capacity. Nothing was charged. Retry the same key after the Retry-After delay.\n- `decoder_busy`: The audio decoder is at capacity. Retry the same key shortly.\n- `audio_decoder_unavailable`: Audio decoding is temporarily unavailable. Retry the same key shortly.\n- `engine_unavailable` (finalized: carries `id` and `billing`; retry with a new key): 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.\n- `request_interrupted` (finalized: carries `id` and `billing`; retry with a new key): The request was interrupted before the engine finished. Not charged. Retry with a new key.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying (sent with `gateway_busy`).",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/capabilities": {
      "get": {
        "tags": [
          "Capabilities"
        ],
        "operationId": "getCapabilities",
        "summary": "Installed languages, dimensions and limits",
        "description": "No API key required.",
        "security": [],
        "responses": {
          "200": {
            "description": "Current capabilities.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Capabilities"
                }
              }
            }
          },
          "503": {
            "description": "The pronunciation engine is unreachable (`engine_unavailable`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/lint": {
      "post": {
        "tags": [
          "Utilities"
        ],
        "operationId": "lintReference",
        "summary": "Check a reference text before recording",
        "description": "Applies the engine’s reference rules (script, digits, length, Mandarin pinyin parsing and character count) without audio. Free; no API key; 120 requests per minute per IP address. A text that passes can still be refused by a newer engine.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "text": {
                    "type": "string"
                  },
                  "language": {
                    "type": "string",
                    "enum": [
                      "zh-CN",
                      "en-US",
                      "fr-FR",
                      "es-419",
                      "de-DE",
                      "it-IT",
                      "pt-BR",
                      "ru-RU",
                      "ko-KR",
                      "ja-JP"
                    ],
                    "default": "zh-CN"
                  },
                  "pinyin": {
                    "type": "string"
                  },
                  "task": {
                    "type": "string",
                    "enum": [
                      "general",
                      "connected_speech",
                      "isolated_word"
                    ],
                    "default": "general"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lint result. `ok` is false when at least one issue has severity `error`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "language",
                    "issues",
                    "units"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "language": {
                      "type": "string"
                    },
                    "units": {
                      "type": "integer",
                      "description": "Characters (Mandarin) or words counted."
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "code",
                          "message",
                          "severity"
                        ],
                        "properties": {
                          "code": {
                            "type": "string"
                          },
                          "message": {
                            "type": "string"
                          },
                          "field": {
                            "type": "string",
                            "enum": [
                              "text",
                              "pinyin"
                            ]
                          },
                          "severity": {
                            "type": "string",
                            "enum": [
                              "error",
                              "warning"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "415": {
            "description": "Send JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Body is not an object of string fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/status": {
      "get": {
        "tags": [
          "Utilities"
        ],
        "operationId": "getStatus",
        "summary": "Synthetic probe status",
        "description": "Outcome of the gateway’s synthetic engine check: one machine-speech clip scored through the /v1/assess engine path every few minutes, without an account or credits. A pass means a well-formed scored result came back; it does not measure accuracy. Free; no API key.",
        "security": [],
        "responses": {
          "200": {
            "description": "Probe summary. Checks that find the engine busy with customer work are skipped (counted in `last_24h.busy_skipped`); three in a row read as `degraded` with a `reason`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "generated_at",
                    "probe",
                    "last_check",
                    "consecutive_failures",
                    "last_24h",
                    "last_7d"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "operational",
                        "degraded",
                        "down",
                        "unknown",
                        "not_monitored"
                      ]
                    },
                    "reason": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "reason_code": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "enum": [
                        "probe_disabled",
                        "consecutive_failures",
                        "engine_saturated",
                        "no_recent_assessment",
                        "recent_failures",
                        null
                      ]
                    },
                    "consecutive_busy": {
                      "type": "integer"
                    },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "probe": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "interval_seconds": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "what": {
                          "type": "string"
                        },
                        "pass_means": {
                          "type": "string"
                        }
                      }
                    },
                    "last_check": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "checked_at": {
                          "type": "string"
                        },
                        "ok": {
                          "type": "boolean"
                        },
                        "http_status": {
                          "type": "integer"
                        },
                        "latency_ms": {
                          "type": "integer"
                        },
                        "model_version": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "error": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    },
                    "consecutive_failures": {
                      "type": "integer"
                    },
                    "last_24h": {
                      "$ref": "#/components/schemas/StatusWindow"
                    },
                    "last_7d": {
                      "$ref": "#/components/schemas/StatusWindow"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/terms": {
      "get": {
        "tags": [
          "Utilities"
        ],
        "operationId": "getTerms",
        "summary": "Supported commercial terms",
        "description": "Plans, credits, limits per plan, what happens to credits after a plan ends, and the invoice shapes billing supports, generated from the code that enforces them. Free; no API key.",
        "security": [],
        "responses": {
          "200": {
            "description": "Commercial terms (`version` dated).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Secret API key (`tp_live_…`) from the dashboard. Keep it on your server. A test key (`tp_test_…`) returns fixed example results and is safe to publish."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "Alternative to the Authorization header."
      }
    },
    "headers": {
      "X-Request-Id": {
        "description": "Request identifier (`req_…`).",
        "schema": {
          "type": "string"
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_multipart",
                  "invalid_api_key",
                  "subscription_required",
                  "insufficient_credits",
                  "payment_balance_due",
                  "upload_timeout",
                  "idempotency_conflict",
                  "request_pending",
                  "idempotency_expired",
                  "upload_too_large",
                  "invalid_idempotency_key",
                  "missing_audio",
                  "invalid_reference",
                  "invalid_task",
                  "invalid_test_outcome",
                  "invalid_input",
                  "unsupported_language",
                  "invalid_audio",
                  "invalid_assessment",
                  "rate_limit",
                  "account_busy",
                  "free_capacity",
                  "gateway_busy",
                  "decoder_busy",
                  "audio_decoder_unavailable",
                  "engine_unavailable",
                  "request_interrupted"
                ],
                "description": "Stable machine-readable code. Branch on this, not on the message."
              },
              "message": {
                "type": "string"
              }
            },
            "additionalProperties": true
          },
          "request_id": {
            "type": "string",
            "description": "Present on errors raised before an assessment was stored."
          },
          "id": {
            "type": "string",
            "description": "Present on finalized failures (stored under the Idempotency-Key)."
          },
          "billing": {
            "$ref": "#/components/schemas/Billing"
          }
        }
      },
      "StatusWindow": {
        "type": "object",
        "properties": {
          "checks": {
            "type": "integer"
          },
          "ok": {
            "type": "integer"
          },
          "success_rate": {
            "type": [
              "number",
              "null"
            ]
          },
          "p50_latency_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "p90_latency_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "busy_skipped": {
            "type": "integer",
            "description": "last_24h only"
          }
        }
      },
      "Billing": {
        "type": "object",
        "required": [
          "credits",
          "balance_credits"
        ],
        "properties": {
          "credits": {
            "type": "integer",
            "minimum": 0,
            "description": "Credits charged by this request (0 when failed or unassessable)."
          },
          "balance_credits": {
            "type": "integer",
            "description": "Remaining plan balance."
          },
          "free_credits": {
            "type": "integer",
            "description": "Remaining free trial credits (every account starts with 50)."
          },
          "source": {
            "type": "string",
            "enum": [
              "free",
              "plan"
            ],
            "description": "Which balance paid for this request. Free credits are spent first."
          }
        }
      },
      "Scores": {
        "type": "object",
        "description": "0–100 display grades. Unsupported dimensions are null; null is never a zero score. `integrity` is completeness.",
        "properties": {
          "overall": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100
          },
          "pronunciation": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100
          },
          "initial": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100
          },
          "final": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100
          },
          "tone": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100
          },
          "integrity": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100
          },
          "fluency": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100
          },
          "rhythm": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100
          }
        },
        "additionalProperties": {
          "type": [
            "number",
            "null"
          ],
          "minimum": 0,
          "maximum": 100
        }
      },
      "Phone": {
        "type": "object",
        "description": "One sound. Mandarin rows describe a syllable component (`dimension` initial, final or tone; tones are integers 1–5). Other languages describe a phoneme with timing and an alignment operation.",
        "properties": {
          "dimension": {
            "type": "string",
            "examples": [
              "tone"
            ]
          },
          "expected": {
            "type": [
              "string",
              "integer",
              "null"
            ]
          },
          "heard": {
            "type": [
              "string",
              "integer",
              "null"
            ]
          },
          "score": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100
          },
          "decision": {
            "type": "string",
            "description": "Correction decision: `review` (strong evidence of an error), `uncertain`, `no_correction`; `unassessable`, `partial` and `legacy_correction` also occur. Treat any other value as no decision.",
            "examples": [
              "no_correction"
            ]
          },
          "flagged": {
            "type": "boolean"
          },
          "start_ms": {
            "type": "integer"
          },
          "end_ms": {
            "type": "integer"
          },
          "operation": {
            "type": [
              "string",
              "null"
            ],
            "description": "Alignment operation (M match, S substitution, …). Non-Mandarin packs."
          },
          "diagnosis_status": {
            "type": "string",
            "description": "`experimental` for non-Mandarin phoneme evidence."
          }
        },
        "additionalProperties": true
      },
      "Word": {
        "type": "object",
        "properties": {
          "word": {
            "type": "string",
            "description": "The word (a character in Mandarin)."
          },
          "pinyin": {
            "type": "string",
            "description": "Mandarin: numbered pinyin of the syllable."
          },
          "expected_ipa": {
            "type": "string",
            "description": "Other languages: expected pronunciation."
          },
          "score": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100
          },
          "start_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "end_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "decision": {
            "type": "string",
            "description": "Other languages: the word-level correction decision. Correction decision: `review` (strong evidence of an error), `uncertain`, `no_correction`; `unassessable`, `partial` and `legacy_correction` also occur. Treat any other value as no decision.",
            "examples": [
              "no_correction"
            ]
          },
          "omission_suspected": {
            "type": "boolean"
          },
          "phones": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Phone"
            }
          }
        },
        "additionalProperties": true
      },
      "Component": {
        "type": "object",
        "description": "A Mandarin syllable component (initial, final or tone).",
        "properties": {
          "expected": {
            "type": [
              "string",
              "integer",
              "null"
            ]
          },
          "heard": {
            "type": [
              "string",
              "integer",
              "null"
            ]
          },
          "agrees_with_prompt": {
            "type": "boolean"
          },
          "score": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100
          },
          "confidence": {
            "type": "string"
          },
          "alternatives": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": [
                    "string",
                    "integer"
                  ]
                },
                "probability": {
                  "type": "number"
                }
              }
            },
            "description": "Ranked candidate labels with probabilities."
          },
          "decision": {
            "type": "string",
            "description": "Correction decision: `review` (strong evidence of an error), `uncertain`, `no_correction`; `unassessable`, `partial` and `legacy_correction` also occur. Treat any other value as no decision.",
            "examples": [
              "no_correction"
            ]
          },
          "advice": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "additionalProperties": true
      },
      "Syllable": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer"
          },
          "expected": {
            "type": "object",
            "properties": {
              "pinyin": {
                "type": "string"
              },
              "initial": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "final": {
                "type": "string"
              },
              "tone": {
                "type": "integer"
              }
            }
          },
          "span_ms": {
            "type": "array",
            "items": {
              "type": [
                "integer",
                "null"
              ]
            },
            "minItems": 2,
            "maxItems": 2
          },
          "score": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100
          },
          "components": {
            "type": "object",
            "properties": {
              "initial": {
                "$ref": "#/components/schemas/Component"
              },
              "final": {
                "$ref": "#/components/schemas/Component"
              },
              "tone": {
                "$ref": "#/components/schemas/Component"
              }
            }
          }
        },
        "additionalProperties": true
      },
      "CompactAssessment": {
        "type": "object",
        "required": [
          "schema",
          "id",
          "assessable",
          "scores",
          "words",
          "billing"
        ],
        "properties": {
          "schema": {
            "const": "toneperfect.compact.v1"
          },
          "id": {
            "type": "string"
          },
          "assessable": {
            "type": "boolean"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "locale": {
            "type": "string"
          },
          "task": {
            "type": "string"
          },
          "model_version": {
            "type": [
              "string",
              "null"
            ]
          },
          "scores": {
            "$ref": "#/components/schemas/Scores"
          },
          "billing": {
            "$ref": "#/components/schemas/Billing"
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "words": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "text": {
                  "type": "string"
                },
                "score": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "units": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "dimension": {
                        "type": "string"
                      },
                      "expected": {
                        "type": [
                          "string",
                          "null"
                        ]
                      },
                      "heard": {
                        "type": [
                          "string",
                          "null"
                        ]
                      },
                      "score": {
                        "type": [
                          "number",
                          "null"
                        ]
                      },
                      "decision": {
                        "type": [
                          "string",
                          "null"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "Assessment": {
        "type": "object",
        "required": [
          "id",
          "assessable",
          "scores",
          "billing"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Request id (`req_…`)."
          },
          "language": {
            "type": "string",
            "examples": [
              "zh"
            ]
          },
          "locale": {
            "type": "string",
            "examples": [
              "zh-CN"
            ]
          },
          "model_version": {
            "type": "string",
            "description": "The model that scored this request."
          },
          "task": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "`completed`, `unassessable` or `partial`."
          },
          "assessable": {
            "type": "boolean"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why the recording was not assessable, e.g. `no_speech / no_audio`, `too_short`, `different_content`, `language_model_not_installed`, `unsupported_phonetic_symbols`, `no_calibrated_units`."
          },
          "scores": {
            "$ref": "#/components/schemas/Scores"
          },
          "words": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Word"
            }
          },
          "syllables": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Syllable"
            },
            "description": "Mandarin only."
          },
          "heard": {
            "type": "object",
            "description": "What the engine estimates was said: `syllables[]` (Mandarin) or `phonetic_transcription`.",
            "additionalProperties": true
          },
          "duration_seconds": {
            "type": [
              "number",
              "null"
            ]
          },
          "timing_ms": {
            "type": "integer",
            "description": "Engine processing time."
          },
          "timing": {
            "type": [
              "object",
              "null"
            ],
            "description": "Mandarin pause and speaking-rate measurements.",
            "additionalProperties": true
          },
          "model": {
            "type": "object",
            "description": "Model metadata: id, calibration notes, limitations.",
            "additionalProperties": true
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "billing": {
            "$ref": "#/components/schemas/Billing"
          },
          "raw": {
            "description": "Engine diagnostics. Not a stable contract."
          }
        },
        "additionalProperties": true
      },
      "Capabilities": {
        "type": "object",
        "properties": {
          "model_version": {
            "type": "string"
          },
          "languages": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "locale",
                "available"
              ],
              "properties": {
                "locale": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "language": {
                  "type": "string"
                },
                "available": {
                  "type": "boolean"
                },
                "status": {
                  "type": "string",
                  "examples": [
                    "experimental"
                  ]
                },
                "model_version": {
                  "type": "string"
                },
                "dimensions": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "tasks": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "correction_level": {
                  "type": "string"
                },
                "limitations": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "unsupported_dimensions": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "notes": {
                  "type": [
                    "string",
                    "array"
                  ]
                },
                "max_words": {
                  "type": "integer"
                },
                "max_duration_seconds": {
                  "type": "number"
                }
              },
              "additionalProperties": true
            }
          },
          "limits": {
            "type": "object",
            "properties": {
              "max_audio_seconds": {
                "type": "integer",
                "examples": [
                  30
                ]
              },
              "max_upload_bytes": {
                "type": "integer",
                "examples": [
                  10485760
                ]
              },
              "concurrent_requests_per_account": {
                "type": "integer",
                "examples": [
                  2
                ]
              }
            }
          }
        },
        "additionalProperties": true
      }
    }
  }
}
