API reference
Domain pack
Tether works out your vocabulary from your own tool names, descriptions and domains, so most tenants never touch this. A domain pack is the escape hatch for when that is not enough: terse tool names, an internal term that never appears in a description, a voice that should not sound like the default. Tenant secret key only.
Set it
PUT /v1/domain-pack
curl -X PUT $TETHER_URL/v1/domain-pack \
-H "X-Api-Key: $TETHER_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"lexicon": { "orders": ["parcel", "shipment"], "returns": ["rma"] },
"prompts": { "tone": "Friendly and brief." },
"outOfScopeMessage": "I can help with orders and returns.",
"handoffMessage": "Our team will reply here within the working day.",
"language": "ka"
}'
{ "registered": true, "version": 1 }
| Field | Shape | Effect |
|---|---|---|
lexicon | An object of domain name to an array of keywords | Extra words that route a question to that domain. They are added to the vocabulary derived from your catalog; they never replace it. |
prompts | An object | The engine reads tone, a string that sets the voice of the assistant, and the two reply-language settings below, replyLanguage and latinGeorgianReply. |
outOfScopeMessage | A string of at most 1000 characters | What the assistant says when a question is outside what your tools can answer. It replaces the instance default. |
handoffMessage | A string of at most 1000 characters | What the customer is told when the conversation is handed to a person, in every language. Without it, a neutral default in the customer's language. |
language | ka or en | The business's own language: the mail Tether sends its staff (sign-in links, approval notices) is written in it. Without it, English. It does not choose the language the assistant answers customers in. |
Every field is optional. A pack you have not registered behaves exactly like one that does not exist.
Reply language
The assistant answers each customer in the language and script of their latest message: Georgian
in Georgian script, Georgian typed in Latin letters in Latin letters, Russian in Russian, English in
English, and any other language in that language. Tether reads the message itself, without the
model, and states the rule in every prompt. Two settings in prompts change it:
| Setting | Values | Effect |
|---|---|---|
replyLanguage | mirror (the default), ka, ru, en | mirror follows each customer. A language answers every customer in that language, whatever they write in. |
latinGeorgianReply | latin (the default), georgian | How a customer who types Georgian in Latin letters is answered: in Latin letters, as they wrote, or in Georgian script. |
{ "prompts": { "tone": "Friendly and brief.", "latinGeorgianReply": "georgian" } }
A customer who asks to be answered in a language ("reply in English") is answered in it, whatever
these settings say: in that turn, and from then on when your backend identifies the person with
X-Tether-User-Id. An unidentified guest's request is not remembered for the next turn, because
every unidentified guest of a business would share it. The order is: the customer's request, then replyLanguage, then
the language of the message. The replies Tether writes without the model (the greeting, the handoff
and out-of-scope defaults, and the notices when the service is busy or over budget) follow the same
decision in Georgian, Georgian in Latin letters, Russian or English, and in English for any other
language. Your own outOfScopeMessage and handoffMessage are one string, used for every language.
Mirroring Latin-typed Georgian is a provisional default: whether to answer it in Georgian script instead is still being decided with pilot data. Every chat response says which language it detected and which it answered in.
Georgian typed in Latin letters
Write Georgian keywords once, in Georgian script. Many customers type Georgian in Latin letters
("sad aris chemi shekveta", "javSani", "sheqveta"), and the router reads such a message in Georgian
script before it matches, so შეკვეთა in your lexicon also matches shekveta, sheqveta and
Sekveta. The reading accepts the common informal spellings: sh/s, ch/c, kh/x,
ts/c/w, dz/z, gh/g, zh/j, q/k, f/p, an apostrophe for the ejective (t'),
and the keyboard-layout capitals (S შ, C ჩ, W ჭ, Z ძ, R ღ, T თ, J ჟ). Keywords match
whole words, so list the inflected forms people type (შეკვეთა, შეკვეთის, შეკვეთაზე), as you
would for Georgian script.
Only a message that reads as Georgian is converted. English, and Russian typed in Latin letters, route exactly as they did, and a Latin keyword in your lexicon still matches the text as typed. The conversion is for routing only: the assistant always receives what the customer wrote. If your customers type a Latin spelling the reading does not produce, add it to the lexicon as it is typed.
Earlier versions
Every version is kept. List them, read one, and put one back in force:
curl "$TETHER_URL/v1/domain-pack/versions" -H "X-Api-Key: $TETHER_SECRET_KEY"
curl "$TETHER_URL/v1/domain-pack/versions/1" -H "X-Api-Key: $TETHER_SECRET_KEY"
curl -X POST "$TETHER_URL/v1/domain-pack/versions/1/restore" -H "X-Api-Key: $TETHER_SECRET_KEY"
GET /v1/domain-pack/versions lists them, GET /v1/domain-pack/versions/{version} reads one, and
POST /v1/domain-pack/versions/{version}/restore puts it back in force. The list is newest first: { "versions": [{ "version": 2, "current": true, "registeredAt": "…" }, …] }.
A version reads back in the shape GET /v1/domain-pack answers. Restoring copies it into a new
version, { "registered": true, "version": 3, "restoredFrom": 1 }, so nothing is rewritten and the
history stays complete. A version that does not exist answers 404 unknown_version.
Read it
GET /v1/domain-pack
{
"version": 1,
"lexicon": { "orders": ["parcel", "shipment"], "returns": ["rma"] },
"prompts": { "tone": "Friendly and brief." },
"outOfScopeMessage": "I can help with orders and returns.",
"handoffMessage": "Our team will reply here within the working day.",
"language": "ka",
"registeredAt": "2026-09-30T08:33:28Z"
}
Before anything is registered it returns version: 0 with empty lexicon and prompts and a
null for both messages and the language.
When it takes effect
The pack is read once per request, so a change applies from the next request onward, not to a turn already running.
Errors
A body that cannot be used answers 400 with registered: false and one message, and registers
nothing:
{ "registered": false, "error": "lexicon domain 'orders' must map to an array of keywords." }
The messages are lexicon must be an object of domain -> [keywords]., the per-domain one above,
prompts must be an object., outOfScopeMessage must be a string.,
outOfScopeMessage must be at most 1000 characters., the same two for handoffMessage,
language must be one of ka, en, or absent for English.,
prompts.replyLanguage must be one of mirror, ka, ru, en, or absent for mirror. and
prompts.latinGeorgianReply must be one of latin, georgian, or absent for latin.
Suggested vocabulary
Vocabulary can also arrive as a suggestion that waits for a person, instead of a new pack
version. That is how a repository's CI runner sends the domain words tether analyze found: its
CI key can file a suggestion with POST /v1/domain-pack/suggestions and
cannot PUT /v1/domain-pack. Your secret key (the dashboard's Repository section) lists pending
suggestions with GET /v1/domain-pack/suggestions and accepts one with POST /v1/domain-pack/suggestions/{suggestionId}/accept, which writes a new pack version adding every
suggested domain the pack does not already name and keeping everything else, or dismisses it. The
routes are on Repository.
Related
- Tools — the catalog the default vocabulary is derived from
- Repository — suggestions from a repository's CI runner
- Concepts: tools