API reference
Embed
What the console's last step does, as four calls: the one snippet your site needs, with the
workspace's widget key; a new widget key when you want one; the snippet and where to paste it,
mailed to the person who runs your site; and a check of what your site actually serves. Tenant
secret key only, like origins: a browser key
is refused with 403 insufficient_scope.
| Method and path | What it does |
|---|---|
POST /v1/embed | The snippet, with the workspace's widget key (issued the first time, the same after) |
POST /v1/embed/rotate | A new widget key in a new snippet; the old key stops working |
POST /v1/embed/instructions | The snippet and where to paste it, mailed to one address ("Send to my developer") |
POST /v1/embed/check | What a page of your site serves: the snippet, its key, the origins and their policy ("Check my site") |
PUT /v1/origins with holdUntilConfirmed | Public origins that wait for your confirmed address instead of being refused |
GET /v1/chat/brand | The colour and logo the widget wears; the widget key may read it |
PUT /v1/chat/brand | Set or clear them (secret key only) |
The snippet
curl -X POST "$TETHER_URL/v1/embed" \
-H "X-Api-Key: $TETHER_SECRET_KEY" -H "Content-Type: application/json" \
-d '{ "language": "ka", "greeting": "რით შემიძლია დაგეხმაროთ?" }'
{
"key": "tpk_5c1d…",
"prefix": "tpk_5c1d9a0e",
"kind": "publishable",
"issued": false,
"revokedPrefix": null,
"endpoint": "https://api.trytether.ge",
"scriptUrl": "https://api.trytether.ge/widget/tether-assistant.js",
"snippet": "<script src=\"https://api.trytether.ge/widget/tether-assistant.js\" defer></script>\n<tether-assistant\n endpoint=\"https://api.trytether.ge\"\n publishable-key=\"tpk_5c1d…\"\n mode=\"floating\"\n lang=\"ka\"\n launcher-label=\"მოგვწერეთ\"\n greeting=\"რით შემიძლია დაგეხმაროთ?\"\n placeholder=\"დაწერეთ კითხვა…\">\n</tether-assistant>",
"origins": ["https://lodge.ge"],
"held": ["https://www.lodge.ge"]
}
The widget key is the workspace's one publishable key for its site. The first call issues it
(issued: true, label "Website widget"); every later call answers the same key, so the snippet
already on your site keeps working and you can come back for it at any time. It is the one key
Tether can show again: a publishable key is public by design (it sits in your page's HTML, may only
chat, stream and send feedback, and a page on an origin you did not register cannot use it), so its
value is kept beside its hash. Every other key is still a hash alone and shown once. The database
allows one live widget key per workspace.
snippet is everything a page needs: the script from your Tether API's own address (no file to host),
and the element with your endpoint, the widget key, a floating launcher, and the first words a
visitor reads in language (ka or English): greeting as you give it (one line, at most 200
characters, escaped), a launcher label and a placeholder. Paste it once, before </body>; the
widget guide has the steps for the common site builders. origins and held
are what /v1/origins holds now.
A secret key never appears in a snippet; building one with any other key is refused.
A new widget key
POST /v1/embed/rotate, same body and answer, with issued: true and the revokedPrefix of the
old key. The old key stops working at once, so the snippet on your site must be replaced. Rotating
the widget key with POST /v1/account/keys/{prefix}/rotate keeps the new
key as the widget key too; revoking it leaves the workspace with none until the next POST /v1/embed.
Origins that wait for your confirmation
A self-serve workspace registers a public origin only once its signup address is confirmed (what "live" means). Asked to, it keeps such an origin waiting instead of refusing it:
curl -X PUT "$TETHER_URL/v1/origins" \
-H "X-Api-Key: $TETHER_SECRET_KEY" -H "Content-Type: application/json" \
-d '{ "origins": ["https://lodge.ge", "https://www.lodge.ge", "http://localhost:8080"], "holdUntilConfirmed": true }'
{ "origins": ["http://localhost:8080"], "held": ["https://lodge.ge", "https://www.lodge.ge"], "reason": "public_origin" }
Local origins are allowed at once, so the snippet works on localhost for testing. The held ones
are refused to a browser exactly as an unregistered origin is (403 origin_not_allowed) until the
address is confirmed; opening the confirmation link allows them in the same step that releases held
mail, with nothing to send again. Without holdUntilConfirmed the request is refused as before,
403 email_unconfirmed with reason public_origin. A PUT replaces both lists; GET /v1/origins
answers { "origins": [...], "held": [...] }. Once the address is confirmed, held is always empty.
Send to my developer
curl -X POST "$TETHER_URL/v1/embed/instructions" \
-H "X-Api-Key: $TETHER_SECRET_KEY" -H "Content-Type: application/json" \
-d '{ "email": "dev@agency.ge", "language": "en", "greeting": "Ask about our rooms." }'
{ "email": "dev@agency.ge", "status": "held", "reason": "third_party_mail", "message": "The snippet and instructions go to dev@agency.ge once this workspace's address is confirmed." }
202. Tether writes the message, not the caller: the snippet exactly as POST /v1/embed builds it
for that language and greeting, where to paste it on a plain HTML site, WordPress (a "Custom HTML"
block, no plugin), Wix, Tilda, Webflow and Shopify, the addresses it answers on, the
Content-Security-Policy to allow if the site sends one, and that the key in it is publishable. No
secret key or signing secret is ever in it; the widget key is read when the message is sent, so a
rotation in between sends the new one. status is queued, held (sent once the workspace's
address is confirmed, as every message to someone else waits; reason third_party_mail) or
not_sent (this deployment sends no mail). It is the one message Tether sends to an address that is
not a registered user, so it is limited to Mail:MaxEmbedInstructionsPerHour (5) a workspace an hour:
over it, 429 embed_mail_limit. A malformed address is 400 invalid_email. Deliveries are listed
with the rest of the mail as embed_instructions.
Check my site
curl -X POST "$TETHER_URL/v1/embed/check" \
-H "X-Api-Key: $TETHER_SECRET_KEY" -H "Content-Type: application/json" \
-d '{ "url": "https://lodge.ge/" }'
{
"url": "https://lodge.ge/",
"reachable": true,
"status": 200,
"finalUrl": "https://lodge.ge/",
"error": null,
"snippet": { "element": true, "script": true, "key": "this_workspace" },
"secretKeyOnPage": false,
"csp": { "scriptAllowed": true, "connectAllowed": false },
"origins": [
{ "origin": "https://lodge.ge", "serves": true, "registered": true, "held": false },
{ "origin": "https://www.lodge.ge", "serves": false, "registered": false, "held": false }
]
}
Tether reads the page the way a browser gets it, through the same outbound guard as every call it
makes to an address it did not choose (an address on a private or loopback network is refused unless
the deployment allows it), following at most five redirects, within Embed:CheckTimeoutSeconds (8)
for the whole check and reading at most Embed:CheckMaxBytes (512 KiB) of the page. It reports
whether the element and its script are on the page, whether the key in it is this workspace's live
widget key (this_workspace), another (other, such as a key since rotated) or absent (missing),
whether the page shows a secret key, what its Content-Security-Policy header allows of this API
(null when it sends none), and, for the address given, where it landed and the other of www. and
the bare domain, whether each serves the site and whether the assistant may answer there. A page
that does not answer in time is reachable: false with the reason in error. 400 invalid_site_url
for an address that is not http or https or carries a user name. The check only reads; it is never
needed to go live.
The widget's brand
The widget on your site wears your brand: the colour and logo Tether read from your site during the website preview, kept when you keep the preview as a workspace. When it mounts, the element reads them with its own widget key:
curl "$TETHER_URL/v1/chat/brand" -H "X-Api-Key: $TETHER_WIDGET_KEY"
{ "color": "#1f7a5a", "logo": "https://lodge.ge/logo.svg" }
Either may be null; a workspace that did not start from a preview has neither, and the widget keeps
Tether's palette. This is the one read a widget key has, and it is a GET from a registered origin
only, like the key's other calls (403 origin_not_allowed from anywhere else). The answer is
Cache-Control: private, max-age=300, so a change reaches a visitor's browser within five minutes.
The colour becomes the widget's accent, the visitor's bubble and its accent-coloured text, with the
ink on it chosen by contrast: white on a dark brand, near-black on a light one, and the avatar's icon
kept dark when the brand is too light to read on white. A page that sets --tether-accent,
--tether-on-accent or --tether-bubble itself still wins (widget guide).
The logo replaces the avatar in the header and the empty state, and the avatar comes back if the
image fails to load.
PUT /v1/chat/brand with the secret key replaces both, which is what the dashboard's Assistant
settings do:
curl -X PUT "$TETHER_URL/v1/chat/brand" \
-H "X-Api-Key: $TETHER_SECRET_KEY" -H "Content-Type: application/json" \
-d '{ "color": "#0a3d62", "logo": "https://lodge.ge/logo.svg" }'
The answer is the stored pair. color is a six-digit hex colour, stored in lower case; logo is an
http or https address of at most 2048 characters with no user name. null or an empty string
clears either. 400 invalid_brand_color or 400 invalid_brand_logo otherwise. A widget key is
refused with 403 insufficient_scope.