Dictionaries API
REST reference for pronunciation dictionaries: list, create, update and delete dictionaries, add entries and bulk replace them from your own glossary.
Custom dictionaries let you control how the TTS pipeline pronounces specific words. Each dictionary is scoped to a project and contains word-to-replacement/IPA mappings that are applied before synthesis.
Use these endpoints to sync dictionaries from your own data sources (PIM, CMS, internal glossary). The TTS-side cache is invalidated after every mutation, so the next synthesis request that uses the dictionary picks up your changes immediately. Authentication uses your project-scoped API key.
Use a dictionary in synthesis
With a project API key, synthesis uses the dictionaries of the key's own project
when the request carries no project_id. To use another project's dictionaries,
or when you authenticate without a project key, send the project_id from the
dictionary response on
/v1/tts/generate or in the
WebSocket config, optionally with dictionary_ids to pick specific dictionaries:
{
"text": "Welcome to KugelAudio.",
"voice_id": 1071,
"language": "en",
"project_id": 42,
"dictionary_ids": [7]
}The SDKs send the same field on every generation and session method:
project_id in Python and projectId in Java and JavaScript. The LiveKit and
Pipecat plugins take it as a constructor option (project_id in Python,
projectId in JavaScript).
Common Request Fields
| Field | Location | Type | Applies to | Description |
|---|---|---|---|---|
project_id | query | integer | Every endpoint on this page | Optional; if supplied, it must match the key's project. |
dict_id | path | integer | Single-dictionary and entry endpoints | Dictionary ID returned by list or create. |
entry_id | path | integer | Update/delete entry | Entry ID returned by list or add. |
The project_id query parameter is omitted from the examples because ordinary
Dashboard API keys are already project-scoped.
Response Objects
Dictionary responses contain:
| Field | Type | Description |
|---|---|---|
id | integer | Dictionary ID |
project_id | integer | Owning project ID |
name | string | Display name |
description | string | null | Optional description |
language | string | null | Optional BCP-47 language filter |
is_active | boolean | Whether the dictionary applies by default |
created_at | string | Creation timestamp |
updated_at | string | Last-update timestamp |
Entry responses contain:
| Field | Type | Description |
|---|---|---|
id | integer | Entry ID |
dictionary_id | integer | Parent dictionary ID |
word | string | Source text to match |
replacement | string | Replacement text sent to synthesis |
ipa | string | null | Optional IPA transcription |
case_sensitive | boolean | Whether matching preserves case |
created_at | string | Creation timestamp |
updated_at | string | Last-update timestamp |
Wrapper responses contain:
| Operation | Fields |
|---|---|
| List dictionaries | dictionaries (array of dictionary objects) |
| List entries | entries (array), total (integer before pagination), limit (integer), offset (integer) |
| Bulk replace | upserted (integer), deleted (integer), total (integer; resulting payload size) |
| Delete dictionary/entry | deleted (boolean, always true on a successful response) |
List Dictionaries
Return every dictionary in the caller's project.
/v1/dictionariesQuery Parameters
project_idOptional; rejected when it disagrees with the key's project.
{
"dictionaries": [
{
"id": 1,
"project_id": 42,
"name": "Brand names",
"description": "Customer product names",
"language": "en",
"is_active": true,
"created_at": "2026-05-20T09:00:00+00:00",
"updated_at": "2026-05-20T09:05:00+00:00"
}
]
}curl -X GET "https://api.kugelaudio.com/v1/dictionaries" \
-H "Authorization: Bearer YOUR_API_KEY"from kugelaudio import KugelAudio
client = KugelAudio(api_key="YOUR_API_KEY")
for d in client.dictionaries.list():
print(f"{d.id}: {d.name} ({d.language})")import { KugelAudio } from 'kugelaudio';
const client = new KugelAudio({ apiKey: 'YOUR_API_KEY' });
const dictionaries = await client.dictionaries.list();
for (const d of dictionaries) {
console.log(`${d.id}: ${d.name} (${d.language})`);
}KugelAudio client = KugelAudio.fromEnv();
for (Dictionary d : client.dictionaries().list()) {
System.out.println(d.getId() + ": " + d.getName());
}Create Dictionary
/v1/dictionariesBody
nameDisplay name (1-200 characters). Must be unique within the project; a duplicate answers 409.
descriptionFree-form description (maximum 2000 characters). Omit or send an empty string for no description.
languageBCP-47 language tag (en, de-DE, ...), maximum 16 characters. Omit or
send an empty string for all languages.
curl -X POST "https://api.kugelaudio.com/v1/dictionaries" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Brand names", "language": "en"}'d = client.dictionaries.create(name="Brand names", language="en")
print(d.id)const d = await client.dictionaries.create({ name: 'Brand names', language: 'en' });
console.log(d.id);Dictionary d = client.dictionaries().create("Brand names", null, "en");
System.out.println(d.getId());Update Dictionary
/v1/dictionaries/{dict_id}Only the provided fields are changed.
Body
nameNew name (1-200 characters).
descriptionNew description (maximum 2000 characters). Send an empty string to clear it;
null leaves it unchanged.
languageNew language tag (maximum 16 characters). Send an empty string to clear it;
null leaves it unchanged.
is_activeDisable a dictionary without deleting it.
curl -X PATCH "https://api.kugelaudio.com/v1/dictionaries/1" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"is_active": false}'client.dictionaries.update(1, is_active=False)await client.dictionaries.update(1, { isActive: false });client.dictionaries().update(1, null, null, null, false);List Entries
/v1/dictionaries/{dict_id}/entriesQuery Parameters
searchCase-insensitive substring filter on word.
limit100Page size, 1-500.
offset0Pagination offset.
{
"entries": [
{
"id": 11,
"dictionary_id": 1,
"word": "Postgres",
"replacement": "post-gres",
"ipa": null,
"case_sensitive": false,
"created_at": "2026-05-20T09:00:00+00:00",
"updated_at": "2026-05-20T09:00:00+00:00"
}
],
"total": 12,
"limit": 100,
"offset": 0
}Add Entry
/v1/dictionaries/{dict_id}/entriesBody
wordWord to match (≤ 200 chars). Must be unique within the dictionary; a duplicate answers 409.
replacementText the engine pronounces instead (≤ 1000 chars).
ipaOptional IPA transcription (maximum 200 characters). Takes precedence over
replacement when set.
case_sensitivefalseMatch the original case exactly.
curl -X POST "https://api.kugelaudio.com/v1/dictionaries/1/entries" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"word": "Postgres", "replacement": "post-gres"}'e = client.dictionaries.entries.add(
dictionary_id=1,
word="Postgres",
replacement="post-gres",
)const e = await client.dictionaries.entries.add(1, {
word: 'Postgres',
replacement: 'post-gres',
});DictionaryEntry e = client.dictionaries().entries().add(
1, new DictionaryEntryInput("Postgres", "post-gres"));Bulk Replace Entries
/v1/dictionaries/{dict_id}/entriesReplace every entry in the dictionary. Entries currently in the dictionary
whose word is not in the supplied list are deleted. The operation is
idempotent — calling twice with the same payload converges to the same final
state.
Body
entriesArray of { word, replacement, ipa?, case_sensitive? } items.
word and replacement are required on every item; their limits and
case_sensitive: false default match Add Entry. Duplicate
word values within the payload are rejected.
{ "upserted": 25, "deleted": 3, "total": 25 }curl -X PUT "https://api.kugelaudio.com/v1/dictionaries/1/entries" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"entries": [
{"word": "Postgres", "replacement": "post-gres"},
{"word": "Kubernetes", "replacement": "koo-ber-net-eez"}
]
}'result = client.dictionaries.entries.replace_all(
dictionary_id=1,
entries=[
{"word": "Postgres", "replacement": "post-gres"},
{"word": "Kubernetes", "replacement": "koo-ber-net-eez"},
],
)
print(result.upserted, result.deleted)const result = await client.dictionaries.entries.replaceAll(1, [
{ word: 'Postgres', replacement: 'post-gres' },
{ word: 'Kubernetes', replacement: 'koo-ber-net-eez' },
]);
console.log(result.upserted, result.deleted);BulkReplaceResult result = client.dictionaries().entries().replaceAll(
1,
List.of(
new DictionaryEntryInput("Postgres", "post-gres"),
new DictionaryEntryInput("Kubernetes", "koo-ber-net-eez")));Update Entry
/v1/dictionaries/{dict_id}/entries/{entry_id}Only non-null fields are sent.
Body
wordNew word (1-200 characters).
replacementNew replacement (1-1000 characters).
ipaNew IPA transcription (maximum 200 characters). Send an empty string to clear
it; null leaves it unchanged.
case_sensitiveError responses
| Status | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | Missing or invalid field. |
| 401 | UNAUTHORIZED | API key missing, invalid, or not project-scoped. |
| 403 | UNAUTHORIZED | API key is scoped to a different project than the target. |
| 404 | NOT_FOUND | Dictionary does not exist, or an entry requested for update does not exist. |
| 409 | VALIDATION_ERROR | The name is already used by another dictionary in the project, or the word by another entry in the dictionary. Choose a different one, or edit the existing one. |
| 503 | INTERNAL_ERROR | Dictionary management is unavailable on this deployment. |