Tether
DocsGitHub

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 }
FieldShapeEffect
lexiconAn object of domain name to an array of keywordsExtra words that route a question to that domain. They are added to the vocabulary derived from your catalog; they never replace it.
promptsAn objectThe engine reads tone, a string that sets the voice of the assistant, and the two reply-language settings below, replyLanguage and latinGeorgianReply.
outOfScopeMessageA string of at most 1000 charactersWhat the assistant says when a question is outside what your tools can answer. It replaces the instance default.
handoffMessageA string of at most 1000 charactersWhat 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.
languageka or enThe 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:

SettingValuesEffect
replyLanguagemirror (the default), ka, ru, enmirror follows each customer. A language answers every customer in that language, whatever they write in.
latinGeorgianReplylatin (the default), georgianHow 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.