API reference
Preview Mode
POST /v1/preview reads a business's own public website so they can see their assistant before
doing any integration work. What comes back is explicitly not live, and says so on every answer.
curl -X POST $TETHER_URL/v1/preview \
-H "X-Api-Key: $TETHER_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "siteUrl": "yourshop.com", "feedUrl": "https://yourshop.com/feed.xml" }'
{
"ingested": true,
"site": "yourshop.com",
"pages": 24,
"feedItems": 180,
"chunks": 402,
"pagesRefusedByRobots": 3,
"stoppedAtPageLimit": false
}
feedUrl is optional. RSS, Atom and Google-style product feeds are all read; a product feed is
better preview content than anything crawled out of page markup, because it carries the price,
availability and brand as fields rather than as prose. Without one, a feed the first page announces
for itself (<link rel="alternate" type="application/rss+xml"> or Atom, on the same site, and not
refused by robots.txt) is read instead.
GET /v1/preview reports what is stored and whether the tenant is still preview-only.
DELETE /v1/preview removes it.
What an answer looks like
Every answer drawn from crawled content is flagged, on the envelope and on each block:
{
"meta": { "preview": true, "previewSources": ["https://yourshop.com/delivery"] },
"blocks": [ { "type": "text", "preview": true, "markdown": "…" } ]
}
meta.previewSources names the crawled pages (or feed links) the answer drew on, at most five, so a
surface can say where it came from. The streamed answer (/v1/chat/stream) carries the same
label and sources on its completion frame. Render that label. A preview answer presented as if it were live is the one failure this
feature exists to prevent.
The model is told, in its own instructions, that it cannot see live stock, orders, prices or account details, that it can take no action, and that it must say so rather than quote a figure from a page as if it were current.
When the label goes away
An answer is a preview answer when it drew on crawled content and called no tool.
Once you register a catalog, a stock question routes to your tool, the answer comes from your system, and it is not flagged — a turn that reached your endpoint is grounded in your live data, and labelling it preview would be wrong in the direction that sounds safer. Your public content stays as background knowledge and stays labelled when it is what answered.
What it will not do
The crawl is bounded and polite, because the address comes from whoever typed it:
- Your
robots.txtis obeyed — the most specific group, longest matching rule - The site's own host only, so a link out does not pull in someone else's content
- Bounded by page count, total bytes and per-page bytes
- HTTPS and public addresses only, through the same outbound guard as every other call
- Bounded, before signup, by tighter limits of its own (below)
Before signup: the preview sandbox
POST /v1/preview/sandbox is Preview Mode for somebody with no workspace yet: the onboarding
console's "Paste your website". It answers without a credential, like
signup, and is closed until an operator sets PreviewSandbox:Enabled
(Configuration); closed, it answers
404 sandbox_disabled.
curl -X POST $TETHER_URL/v1/preview/sandbox \
-H "Content-Type: application/json" \
-d '{ "siteUrl": "yourshop.com" }'
{
"sandboxKey": "tsb_…",
"keyPrefix": "tsb_3f9a…",
"state": "crawling",
"site": "yourshop.com",
"expiresAt": "2026-10-17T10:40:00Z",
"limits": { "pages": 8, "bytes": 1048576, "seconds": 20, "lifetimeMinutes": 60, "turns": 20 },
"notice": "A preview of your public site, labelled preview on every answer. …"
}
The site is read in the background, so the answer comes at once. sandboxKey is shown once; it is
a sandbox credential (tsb_) and reaches exactly three things:
| Request | Does |
|---|---|
GET /v1/preview/sandbox | The sandbox's status: state (crawling, ready, empty, failed or expired), pagesRead while it reads, then chunks, topics (up to four page or feed titles, without the site name a title repeats, for suggested questions), name (that site name: the segment its page titles share, else a home page titled only with it, or null; the console offers it as the workspace's name), items (how many listed items were read, below), brand ({ "color": "#rrggbb" or null, "logo": URL or null }, or null), turnsLeft, expiresAt, error and errorCode (site_challenged when the site answered with a bot challenge) |
POST /v1/chat, POST /v1/chat/stream | The preview-only assistant over what was read. Every answer drawn from it is labelled as above. No write mode, no image and no deep analysis, whatever the body asks; the sandbox has no capabilities, so there is nothing to call or stage |
POST /v1/preview/sandbox/claim | Keep it: { "email": "…", "name": "…" } turns the sandbox into a free workspace with the same preview content, through signup's own checks and answers (202 and a mailed link when Signup:VerifyEmail is on, then POST /v1/signup/verify; otherwise 200 with the one-time apiKey, a tth_ secret key). The sandbox key is revoked when the workspace exists |
Anything else answers 403 insufficient_scope with error.reason not_a_sandbox_route, and a
workspace's own secret key on the two sandbox routes answers sandbox_credential_required.
What a crawl reads besides prose
- Brand. The home page's
theme-color, else its web app manifest'stheme_color, else a CSS custom property named for the brand (--primary,--brandand their kin) in its inline styles or first two stylesheets on the site, else the most used saturated color there. Greys, near-white and near-black never count. The logo is an image namedlogo, else the touch icon, an SVG or large icon,og:logo, then/favicon.ico;httpsonly unless insecure endpoints are allowed. - Listed items.
application/ld+jsonon every page read (products, offers, services, rooms, events, places and their kin), Next.js__NEXT_DATA__, and public JSON endpoints the pages themselves reference: an/api/path or a.jsonfile on the site's host or a subdomain of it, with no placeholders and nothing under an auth, account, cart or user path. Each is oneGETwithout credentials, its host'srobots.txtobeyed, at mostMaxDataEndpointsof them andMaxItemsitems in all, within the crawl's byte and time budget. Arrays of named objects become one line per item, stored as preview content like any page, so answers from them are labelled preview too. - A bot challenge. A site whose first page answers with Cloudflare's challenge (
cf-mitigated: challenge, or its interstitial) fails at once witherrorCodesite_challenged. Tether never tries to pass a challenge; the site's owner can allow the preview's user agent.
What bounds it:
- The crawl. At most
MaxPagespages,MaxTotalBytesacross the crawl andMaxPageBytesper response, withinCrawlSecondsof wall time (what was read by then is kept);robots.txtobeyed, the site's own host only, and a private, loopback or unresolvable address refused with400 site_refusedbefore anything is created. - Who may start one.
MaxPerAddressPerDayper source address (stored as a salted hash, as signup's) andMaxGlobalPerHouracross everybody, both429 rate_limited; expired and claimed sandboxes still count. At mostMaxConcurrentCrawlsread at once on an instance; beyond that,503 sandbox_busy, never a queue. - What it may spend.
MaxTurnsPerSandboxanswers andSandboxBudgetUsdof model spend per sandbox, andDailyBudgetUsdfor every sandbox together in a UTC day. Exhausted, a turn answers429 sandbox_budget_exhaustedwitherror.reasonsandbox_limitordaily_limit, before any model is called. A budget of0refuses every turn. - How long it lives.
LifetimeMinutes, after which turns answer410 sandbox_expiredand the sweep deletes the sandbox, its pages, conversations and key. A claim waiting for its mailed link keeps the sandbox for as long as the link is good.
A claimed sandbox is an ordinary free workspace: everything on this page above applies to it, and everything else in the API is reachable with its secret key.
Related
- Use Tether AI — step one of five
- Context and memory — where preview content is stored