Tether
DocsGitHub

Reference

Block types

Nine types, one version. Every block carries a type and a stable id; everything else depends on the type.

The document

{
  "content_type": "application/vnd.tether.blocks.v1+json",
  "version": "tether.blocks.v1",
  "title": "Optional heading",
  "blocks": [ ],
  "meta": { "time_range": { "start": "…", "end": "…" } }
}

content_type and version are constants. blocks has at least one entry. meta is always an object. title is optional.

Unknown top-level keys and unknown block types are rejected — the document is validated strictly, then repaired or degraded rather than passed through. See The output contract.

text

Prose. The most common block by a wide margin.

{ "type": "text", "id": "summary", "variant": "body",
  "markdown": "Seven tickets are open. **T-4417** has been open 41 minutes." }
FieldRequiredNotes
markdownyesNon-empty. Inline markdown — bold, italic, links, code
variantnobody | caption | title

kpi

One or more headline numbers.

{ "type": "kpi", "id": "headline", "title": "Today",
  "items": [
    { "label": "Open tickets", "value": 7 },
    { "label": "Average wait", "value": 12.5, "delta": -0.18, "intent": "positive" }
  ] }
FieldRequiredNotes
itemsyesAt least one
items[].labelyesNon-empty string
items[].valueyesNumber, never a formatted string
items[].deltanoNumber. A fraction, not a percentage — 0.12 is +12%
items[].intentnopositive | neutral | negative
titlenoString

intent is a judgement, not a sign. Costs falling is positive even though the delta is negative — render the colour from intent and the arrow from delta.

table

Columnar data with declared columns.

{ "type": "table", "id": "by_vendor", "title": "Spend by vendor",
  "columns": [
    { "key": "vendor", "label": "Vendor", "align": "left" },
    { "key": "amount", "label": "Amount", "align": "right" }
  ],
  "rows": [
    { "label": "Northside Produce", "amount": 3120.4 },
    { "label": "Harbor Meats", "amount": 2870 }
  ],
  "ui_hints": { "sortable": true } }
FieldRequiredNotes
columnsyesAt least one
columns[].keyyesMatches a key on each row
columns[].labelyesHeader text
columns[].alignnoleft | center | right
rowsyesAt least one object
title, ui_hintsno

Render only the declared columns, in the declared order. A row may carry keys no column names — ignore them rather than rendering a ragged table.

ranked_list

An ordered list where position carries meaning.

{ "type": "ranked_list", "id": "movers", "title": "Biggest movers",
  "rows": [
    { "label": "Produce", "value": 3120.4, "rank": 1, "trend": "up", "note": "up 31% on last month" },
    { "label": "Dairy", "value": 1980, "rank": 2, "trend": "flat" }
  ] }
FieldRequiredNotes
rowsyesAt least one object
title, ui_hintsno

stacked_bar

A single bar split into parts. A composition, not a time series.

{ "type": "stacked_bar", "id": "spend_split", "title": "Where it went",
  "value_label": { "prefix": "$", "text": "8,420", "unit": "USD" },
  "segments": [
    { "key": "produce", "label": "Produce", "value": 3120.4, "token": "series_1" },
    { "key": "meat", "label": "Meat", "value": 2870, "token": "series_2" },
    { "key": "dry", "label": "Dry goods", "value": 2429.6, "token": "series_3" }
  ],
  "ui_hints": { "show_legend": true } }
FieldRequiredNotes
segmentsyesAt least two. One segment is not a composition
segments[].keyyesStable identifier
segments[].labelyesDisplay text
segments[].valueyesNumber, zero or greater
segments[].tokenyesOne of the theme tokens below
value_labelnotext, prefix, suffix, unit
title, ui_hintsno

cards

Items a visitor chooses between: products, offers, listings, services. Drawn as a carousel, with filter chips when there are groups.

{ "type": "cards", "id": "options", "title": "Children's fever relief",
  "groups": [ { "key": "paracetamol", "label": "Paracetamol" }, { "key": "ibuprofen", "label": "Ibuprofen" } ],
  "cards": [
    { "id": "sku-118", "title": "Paracetamol syrup 120mg/5ml 125ml", "eyebrow": "Paracetamol",
      "group": "paracetamol", "image": "https://cdn.example.com/sku-118.png",
      "price": { "amount": 7.92, "was": 10.15, "currency": "GEL" },
      "availability": { "state": "in_stock", "label": "In stock" },
      "ask": { "label": "Details", "prompt": "Tell me more about sku-118" } }
  ] }
FieldRequiredNotes
cardsyesAt least one; Tether keeps the first 12
cards[].id, cards[].titleyes
cards[].eyebrow, subtitlenoBrand or category above the title; one line below it
cards[].imagenohttps only, and only an address a tool result of this turn contained
cards[].priceno{ amount, was?, currency }, numbers from a tool result. A discount is computed from was
cards[].availabilityno{ state, label? }, state one of in_stock, low_stock, out_of_stock, unknown
cards[].factsnoShort strings; Tether keeps the first 6
cards[].groupnoThe key of one of the block's groups
cards[].askno{ label, prompt }: a button that sends prompt as the next question
cards[].phone, cards[].locationnoA call button; { lat, lng } for a directions button
groupsno{ key, label } filter chips, counted and filtered on the page
titleno

places

Locations a visitor goes to or contacts: branches, shops, offices, service points. A list with hours, distance and buttons, and a map when the page provides tiles.

{ "type": "places", "id": "branches", "title": "Tbilisi: all closed now", "total": 40,
  "timezone": "Asia/Tbilisi",
  "places": [
    { "id": "n109", "name": "N109 (Tavdadebuli St)", "address": "Tavdadebuli St 40a",
      "location": { "lat": 41.7841, "lng": 44.7712 }, "phone": "577 23 16 09",
      "distance_km": 2.4, "duration_min": 9,
      "hours": [ { "days": "mon-sun", "open": "08:00", "close": "22:00" } ] }
  ] }
FieldRequiredNotes
placesyesAt least one, nearest or most relevant first; Tether keeps the first 30
places[].id, places[].nameyes
places[].address, noteno
places[].locationno{ lat, lng } from a tool result: a directions button and a map pin
places[].phonenoFrom a tool result: a call button
places[].distance_km, duration_minnoFrom a tool result only, never from a guess about where the visitor is
places[].hoursno[{ days, open, close }], days a day or range of mon…sun, times HH:MM. A close earlier than the open runs past midnight
places[].asknoAs in cards
timezonenoThe IANA zone the hours are in. The renderer, not the model, says whether a place is open now
totalnoHow many exist when fewer are shown
title, subtitleno

Provenance

An image, a price, a phone number, a coordinate, a distance or a travel time in a cards or places block must appear in a tool result of the same turn. Tether removes any that does not before the answer leaves, and the answer still renders without it. The model chooses and arranges; it does not supply the facts a visitor acts on. There are no link fields: a button asks a question, dials digits, or opens directions built from coordinates.

quick_replies

Suggested follow-up questions.

{ "type": "quick_replies", "id": "followups",
  "replies": [
    { "id": "r1", "label": "Show the oldest" },
    { "id": "r2", "label": "Group by server" }
  ] }
FieldRequiredNotes
repliesyesAt least one
replies[].idyesStable within the block
replies[].labelyesThe text to send back as the next prompt

footnote

A caveat about the answer above it — a truncated result, a stale figure, an assumption.

{ "type": "footnote", "id": "caveat", "text": "Excludes tickets closed before 06:00." }
FieldRequiredNotes
textyesPlain text. No markdown, no other fields

Theme tokens

Colour is chosen by the model from a fixed set of semantic tokens, never as a hex value. Your renderer maps each token to your palette, so an answer looks like your product and adapts to dark mode without the model knowing either exists.

TokenIntended use
series_1 … series_6Categorical series in a chart
section_positiveA favourable grouping
section_warningA grouping needing attention
section_actionA grouping requiring action
severity_goodHealthy
severity_neutralInformational
severity_warningDegraded
severity_criticalUrgent

Any other value fails validation. There is no eighth series token — a chart needing more than six categories should aggregate a tail.

ui_hints

Advisory. Ignoring any of them is valid.

HintType
compactboolean
sortableboolean
show_legendboolean
x_labelstring
y_labelstring

meta

{
  "time_range": { "start": "2026-07-21T00:00:00Z", "end": "2026-08-20T00:00:00Z" },
  "sources": [{ "id": "chunk_41", "label": "Refund policy", "type": "knowledge" }]
}

time_range is server-owned: Tether writes it from the request's period and overwrites whatever the model produced. It is not validated against the model's output for exactly that reason — an otherwise perfect answer should not be thrown away over one field the model cannot know better than the server does.

sources appears when retrieval contributed and IncludeSourcesInMeta is on. Each entry needs id, label and type.

Numbers are numbers

Any field named value, amount, amount_estimate, confidence, rank, currentQuantity, alertPoint, deficit, reorderQuantity or shortagePercent must be a JSON number. "84.50" fails validation wherever it appears in the document.

Formatting — currency symbols, thousands separators, locale — is your renderer's job, which is half the point of the contract.

Row fields

Rows in table and ranked_list are constrained by the schema the model generates against. Alongside the general fields — id, label, value, rank, amount, note, secondary, trend, category, unit, status, severity, confidence, token — the schema carries a handful of narrower names such as itemName, storageName, alertPoint, deficit, reorderQuantity, shortagePercent and recommendedAction.