Glossary Validation API
POST /api/v1/glossary/validate
Description
Checks a glossary (terminology, fuzzy correction, translation dictionary) for format problems and internal conflicts before it is saved.
Most glossary problems produce no error message at all — they simply yield unexpected results, silently, during recording or translation. For example, when an incorrect variant is also the correct term of another rule, saying that word normally gets it rewritten into something else. This endpoint surfaces problems of that kind at the moment the user presses Save, along with the index of each offending entry, so a management UI can flag the exact item.
When to use it: from your glossary management UI, before saving. It is not necessary to call it before every recording or audio import.
Note: This endpoint validates the glossary itself. Passing validation does not guarantee that audio import will accept the same glossary — import applies stricter rules.
Host
Use the same domain you connect to over WebSocket, swapping the wss:// scheme for https://. This endpoint is served by the realtime service domain, which may differ from the domain serving the other REST endpoints; use the connection settings you were issued.
# If your WebSocket connection is wss://<realtime-host>/ws
curl -X POST "https://<realtime-host>/api/v1/glossary/validate"
Authentication
Header: X-API-Key (see Authentication)
Billing
Free. No points are deducted, no task or recording is created, and no glossary settings are written — it is a pure check.
Rate Limit
120 requests per minute per API Key, as a separate quota counted independently of the other REST endpoints.
Responses that got past authentication always carry the headers below (the quota is counted per API Key, and on an authentication failure the key is not yet known, so 401 and 403 responses do not carry them). When the limit is exceeded the endpoint returns HTTP 429 with Retry-After:
| Header | Description |
|---|---|
X-RateLimit-Limit | Requests allowed per minute |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Seconds until the current window ends |
Retry-After | Provided on 429 only; the recommended wait in seconds |
Request Size Limit
A single request body is limited to 2 MB (2,097,152 bytes) by default. A larger body returns HTTP 413 config_payload_too_large, with details.max_bytes carrying the limit currently in effect.
This limit may change. Always read
details.max_bytesfrom the response rather than hard-coding a value.
If a complete glossary exceeds the limit, it can be split across two requests, but not arbitrarily:
| Request | Contents | Notes |
|---|---|---|
| First | terminology + fuzzy_correction | Must stay together. The conflicts that span these two sections (variant_shadows_term, homophone_conflict) can only be found when both are present in the same request |
| Second | translation_dict | Can be sent on its own. The translation dictionary takes part in no cross-section conflict detection |
Note: When
terminologyandfuzzy_correctionare sent separately,variant_shadows_termandhomophone_conflictare not detected, and the response gives no indication of it —has_conflictsstill comes backfalse. If you use this endpoint as a save-time gate, keep those two sections in the same request.
Request Parameters
Every field is optional — whatever you send is what gets validated.
| Parameter | Type | Required | Description |
|---|---|---|---|
terminology | object | No | Terminology, in the same format as config (language code → array of terms) |
fuzzy_correction | object | No | Fuzzy correction, in the same format as config |
translation_dict | object | No | Translation dictionary, in the same format as config (the language code is the target language) |
check_homophones | boolean | No | Whether to check for homophone conflicts. Defaults to true. See below |
transcription_languages | array | No | The source languages to check. When omitted, they are derived from the glossary's own language codes |
translation_languages | array | No | Reserved field; currently does not affect the result (still subject to the language-code count limit) |
Fields other than those listed above are ignored. If your glossary management interface holds a complete
configobject, you can send it as-is without stripping extra fields.
For the detailed format of the three glossary blocks, see the Terminology Guide.
check_homophones: Editing and Saving Are Checked Differently
Of the eight conflict types, only homophone conflicts (homophone_conflict) require an additional pronunciation comparison; the rest are plain string comparisons that complete in milliseconds.
- Live checks while the user edits → send
false; safe to call frequently - When the user actually saves → omit it or send
truefor the full check
Important: The homophone check applies to Chinese only, and may not finish while the service is busy.
homophones_checkedin the response is what tells these two situations apart:
homophones_checked: true→ homophone conflicts have been fully checkedhomophones_checked: false→ the check did not complete; this does not mean there are no conflictsA save gate that looks only at
has_conflictslets an unchecked glossary through wheneverhomophones_checkedisfalse.
Request Example
curl -X POST "https://<realtime-host>/api/v1/glossary/validate" \
-H "X-API-Key: vas_aB3dE5fG7hI9jK1lM3nO5pQ7rS9tU1vW" \
-H "Content-Type: application/json" \
-d '{
"fuzzy_correction": {
"zh-TW": [
{ "correct": "報價", "incorrect": ["抱歉"] },
{ "correct": "抱歉" }
]
}
}'
Success Response
HTTP 200
HTTP 200 means the request itself was valid, not that the glossary is problem-free. The glossary's results are always inside
data; readvalidandhas_conflicts.
{
"type": "glossary_validation",
"data": {
"valid": false,
"has_conflicts": true,
"homophones_checked": false,
"exceeds_runtime_limits": false,
"checked": ["fuzzy_correction"],
"languages_used": ["zh-TW"],
"limits": {
"terminology_max": 500,
"fuzzy_rules_max": 6000,
"dict_entries_max": 4500
},
"conflicts": [
{
"code": "variant_shadows_term",
"severity": "error",
"variant": "抱歉",
"occurrences": [
{ "language": "zh-TW", "index": 0, "variant_index": 0, "term": "報價" }
],
"shadows": [
{ "language": "zh-TW", "index": 1, "term": "抱歉", "kind": "fuzzy_correct" }
]
}
],
"errors": [],
"warnings": { "unknown_languages": [] }
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
valid | boolean | The save gate. false when there is any format error or any conflict with severity: "error" |
has_conflicts | boolean | Whether there is any conflict at all, warnings included. Note: See "No Conflict Analysis When the Counts Are Over the Limit" below |
homophones_checked | boolean | Whether homophone conflicts were fully checked. See above |
exceeds_runtime_limits | boolean | The glossary can be saved, but sending it in full into a recording would exceed the limits. See below |
checked | array | Which blocks were actually checked on this call |
languages_used | array | The language codes visited on this call (the union across the three blocks) |
limits | object | The limits this endpoint applies |
conflicts | array | The list of conflicts. See the next section |
errors | array | Format and quantity problems, in the same shape as the error response of config |
warnings | object | Currently only unknown_languages: language codes that could not be recognized |
truncated | boolean | Present only when content was truncated for exceeding the reporting cap (either conflicts or format problems can be truncated) |
totals | object | Present only when conflicts were truncated; conflicts is the total number of conflicts before truncation. No total is provided when format problems were truncated |
The top-level array fields of
dataalways return[]when empty, nevernull. Optional fields inside a conflict object (occurrences/shadows/languages/termsand the like) are omitted entirely when they have no content; they never appear as empty arrays.
No Conflict Analysis When the Counts Are Over the Limit
When the glossary holds more entries than the limits allow, this endpoint reports only the count problem itself. It does not go on to check the entries one by one, and it performs neither conflict detection nor the homophone check. In that case the response reads:
valid: false, with the count problem inerrorshas_conflicts: false,conflicts: [],homophones_checked: false
This does not mean the glossary is free of conflicts — it only means nothing has been checked yet. Follow errors to bring the counts back within the limits, then validate again to get the full conflict list.
A save gate that looks only at
has_conflictswill read this as "no problems". Always readvalidfirst.
exceeds_runtime_limits
This endpoint allows a glossary slightly larger than the amount actually usable during recording, so a management UI does not get stuck while the user is still editing.
exceeds_runtime_limits: true means the glossary can be saved, but sending it in full into a recording would be rejected. Prompt the user to trim it, or to split it across several glossaries.
Conflict Types
Every conflict carries code, severity, and the position of the problem (index is the position within that language's array, counted from 0), so a management UI can flag the entry directly.
The response contains no human-readable text. Compose your own wording from code and the fields — the language and phrasing are entirely yours to decide.
code | severity | Meaning |
|---|---|---|
variant_shadows_term | error | An incorrect variant is also the correct term of another rule, or a term in the terminology block |
dict_duplicate_source | error | Under the same target language, the same source is registered more than once |
variant_ambiguous | warning | The same incorrect variant maps to different correct terms |
case_flag_conflict | warning | The same incorrect variant has inconsistent case_insensitive settings across rules |
variant_equals_term | warning | An incorrect variant is identical to the correct term of its own rule |
duplicate_term | warning | The same term is registered more than once under the same language family |
boost_out_of_range | warning | A term's boost is outside the valid range and will be adjusted automatically |
homophone_conflict | warning | Two correct terms or terminology entries share a pronunciation |
Common Fields
| Field | When present | Description |
|---|---|---|
code | Always | The conflict type |
severity | Always | error / warning |
variant | Variant conflicts | The incorrect variant at fault |
term | Terminology conflicts | The term at fault |
source | Dictionary conflicts | The source word at fault |
at | Single-position conflicts | The one position at fault |
occurrences | Multi-position conflicts | All related positions |
occurrences_total | When positions were truncated | The total before truncation |
The position object (the elements of at and occurrences):
| Field | Description |
|---|---|
language | Language code |
index | The position within that language's array (counted from 0) |
variant_index | The position within that entry's incorrect array (counted from 0); provided for variant conflicts only |
term | The result at that position: the rule's correct term, the term itself, or the dictionary's translation |
kind | The origin of that position: fuzzy_correct (a fuzzy correction rule's correct term) / terminology_term (a term in the terminology block) / dict_source (an entry in the translation dictionary) |
variant_shadows_term (error)
An incorrect variant is also a correct term elsewhere. When the user says that word normally, it is rewritten into something else.
The shadows array lists every position shadowed by this variant (truncated when there are too many, with shadows_total added).
{
"code": "variant_shadows_term",
"severity": "error",
"variant": "抱歉",
"occurrences": [
{ "language": "zh-TW", "index": 0, "variant_index": 0, "term": "報價" }
],
"shadows": [
{ "language": "zh-TW", "index": 1, "term": "抱歉", "kind": "fuzzy_correct" }
]
}
In the example above, rule 0 treats 抱歉 as a misspelling of 報價, while rule 1 registers 抱歉 as a correct term. The result: when the user says 抱歉, it becomes 報價.
dict_duplicate_source (error)
Under the same target language, the same source word is registered with more than one translation. Only one takes effect; the others are ignored without notice.
effective_index points at which element of occurrences actually takes effect (the last entry in the array wins).
{
"code": "dict_duplicate_source",
"severity": "error",
"source": "報價",
"occurrences": [
{ "language": "en-US", "index": 0, "term": "quotation", "kind": "dict_source" },
{ "language": "en-US", "index": 1, "term": "quote", "kind": "dict_source" }
],
"effective_index": 1
}
variant_ambiguous (warning)
The same incorrect variant maps to different correct terms; only one rule takes effect.
Which rule wins is not reported — the ordering is not guaranteed, least of all across languages. Treat it as something you must fix by choosing one.
When the ambiguity comes from
case_insensitive(for example,ALFAandAlfaare the same variant once casing is ignored),variantis reported in lowercase, which may not exactly match any of the spellings in your glossary. For the original spellings, readoccurrences[]— every element points at an exact position.
case_flag_conflict (warning)
The same incorrect variant has inconsistent case_insensitive settings across rules.
effective_case_insensitive is always false: if any one rule leaves it disabled, the variant is matched strictly.
variant_equals_term (warning)
An incorrect variant is identical to the correct term of its own rule (with case_insensitive enabled, differing only in casing counts as identical too).
It produces no incorrect result, but the entry has no effect whatsoever while still consuming quota.
duplicate_term (warning)
The same term is registered more than once under the same language family. Correction behavior is unaffected, but each duplicate still consumes terminology quota.
A language family is the part of the language code before the first hyphen: zh-TW and zh-CN belong to the same family, so a term registered under one of them applies to the other as well, and registering it under both is a duplicate. zh-TW and ja-JP do not. occurrences may therefore span several language codes.
boost_out_of_range (warning)
value is the value you sent; clamped_to is the value that actually takes effect.
{
"code": "boost_out_of_range",
"severity": "warning",
"term": "語者分離",
"at": { "language": "zh-TW", "index": 0, "term": "語者分離" },
"value": 99,
"clamped_to": 5
}
homophone_conflict (warning)
Two correct terms or terminology entries share a pronunciation. The registered words themselves are unaffected, but which of them a third, unregistered homophonic spelling is assigned to is not determined.
languages and terms have the same shape as in the realtime service's config_updated event, so the same display logic can be shared.
This conflict also carries occurrences, so you can trace where those words are registered in the glossary (kind is either terminology_term or fuzzy_correct) and flag the exact entries in a management UI. When there are too many positions, occurrences_total is added.
{
"code": "homophone_conflict",
"severity": "warning",
"languages": ["zh-TW"],
"terms": ["公事包", "公式包"],
"occurrences": [
{ "language": "zh-TW", "index": 0, "term": "公事包", "kind": "terminology_term" },
{ "language": "zh-TW", "index": 1, "term": "公式包", "kind": "terminology_term" }
]
}
This check applies to Chinese only, and runs only when check_homophones has not been turned off.
Error Responses
| HTTP | error_code | Description |
|---|---|---|
| 400 | invalid_json | The request body is not valid JSON |
| 400 | config_too_many_languages | There are more language codes than allowed (details carries field / count / max). The three glossary blocks and transcription_languages / translation_languages are all covered by this limit |
| 401 | auth_missing_api_key | No X-API-Key was provided |
| 401 | auth_invalid_key_format | The API Key format is not valid |
| 401 | auth_invalid_api_key | The API Key is invalid |
| 401 | auth_key_expired | The API Key has expired |
| 401 | auth_key_disabled | The API Key is disabled |
| 403 | auth_ip_not_allowed | The source IP is not on the allowlist |
| 403 | auth_user_disabled | The account is disabled |
| 403 | auth_account_blocked | The account is blocked |
| 405 | invalid_action | The method is not POST. The response carries an Allow header listing the supported methods |
| 413 | config_payload_too_large | The request body exceeds the size limit (2 MB by default; details.max_bytes is the limit in bytes currently in effect — see Request Size Limit) |
| 429 | too_many_requests | The rate limit was exceeded |
| 500 | auth_service_error | The authentication service is temporarily unavailable |
Note: On
auth_service_error, retry — do not swap the key. It means our authentication service is temporarily unavailable and has nothing to do with the key itself; the 401s are the ones that point at the key.
Problems with the glossary itself never return 4xx — those are always inside conflicts and errors on an HTTP 200.
Cross-Origin Requests
This endpoint can be called directly from a browser: OPTIONS preflight is allowed, and responses carry a full set of cross-origin headers (including Access-Control-Expose-Headers, so the browser can read the rate-limit headers and Retry-After). Error responses carry them as well, so the browser can read the body of a 401, 413, or 429.
Important: Calling from a browser means your API key is present in the browser.
This endpoint uses the same API key as every other endpoint, and that key can also create recordings, import audio, and consume credits. Anyone who can open the page can retrieve it.
Therefore:
- If your glossary management interface is an internal admin tool (visible only to your own signed-in administrators), the risk is manageable and calling directly from the browser is fine
- If it is a public page, route the call through your own backend instead — do not put the API key in the browser
Calls made from your own backend are not affected by cross-origin restrictions.
Version: V1.24.1 Last Updated: 2026-09-28