API reference
Authentication
One header authenticates every request. Three optional headers narrow it. Nothing in a request body ever changes who is asking.
The key
curl https://api.trytether.ge/v1/tools \
-H "X-Api-Key: tth_9f3c…"
Authorization: Bearer tth_9f3c… is accepted as an equivalent — some clients and gateways make
the custom header awkward. Both resolve to the same tenant.
Every path except /health, /swagger and /openapi requires a key.
Kinds of key
What a credential may reach depends on which kind it is, and the prefix says which.
| Prefix | Kind | Reaches |
|---|---|---|
tth_ | Secret | The whole tenant API. Server-side only |
tag_ | Agent | POST /v1/chat, POST /v1/chat/stream, POST /mcp. Nothing else |
tpk_ | Publishable | POST /v1/chat and POST /v1/chat/stream. Safe to ship in a page |
tac_ | Action token | Confirms the single staged write it was minted with |
tpv_ | Provider | /v1/provider: creating and managing the businesses a software provider serves, and /v1/user-sessions. See Providers |
tib_ | Inbox | Asking Tether to mail a sign-in link, spending it, and opening and closing user sessions; with a session, the approval routes as that person. Nothing else (error.reason not_an_inbox_route, or user_session_required without a session) |
tsb_ | Preview sandbox | Issued with no other credential by POST /v1/preview/sandbox: its own GET /v1/preview/sandbox, POST /v1/chat and POST /v1/chat/stream with the preview-only assistant, and POST /v1/preview/sandbox/claim, which revokes it. Nothing else (error.reason not_a_sandbox_route). See Preview Mode |
tci_ | CI | What tether publish --send sends from a repository's own runner: the import's draft step, knowledge documents with a repository source, a vocabulary suggestion, and reading the pack, the knowledge list and itself. It never changes the live pack, policy, catalog or a key. See Repository (error.reason not_a_ci_route, ci_draft_only, ci_source_required or ci_replace_not_allowed) |
An agent key is the credential your assistant runs with. It stages writes and reads your catalog; it cannot register a capability, change your approval policy, point the tool endpoint somewhere else, or issue credentials. That separation is the point: an agent that can rewrite the declaration governing it is not governed by it, so registering capabilities is something your secret key does during onboarding and your agent never does at all.
Anything not named above is refused for every kind but the secret key, so a route added later is
closed to a browser or an agent credential until someone opens it deliberately. /v1/provider is
the one exception the other way: no secret key reaches it, only a provider key, which Tether issues
when it grants a platform provider authority.
How keys are stored
A key is tth_ followed by 48 hex characters. Tether stores only its SHA-256 hash and a
12-character non-secret prefix, so:
- The plaintext is shown once, at issuance. There is no way to read it back.
- A database disclosure does not hand anyone the ability to spend your budget.
- The prefix lets a key be identified in a list without revealing it.
You issue, rotate and revoke your own keys, and recover from a leaked one in a single call, with your secret key: see Account. Only plan changes and keys for a tenant other than your own are operator actions.
Failure modes
Both of these return 401 with an identical shape:
{ "error": { "code": "missing_api_key", "message": "Provide an API key in the X-Api-Key header." } }
{ "error": { "code": "invalid_api_key", "message": "The API key is not valid." } }
An unknown key and a revoked key are deliberately indistinguishable. Telling a caller which half of a guess was right is a free hint.
The scope headers
| Header | Type | Meaning |
|---|---|---|
X-Tether-Scope-Id | positive integer | A narrower boundary inside the tenant — a site, ledger, region, workspace |
X-Tether-User-Id | positive integer | The individual acting. Attributed on staged writes and memory |
X-Tether-User-Role | string | Passed through to your endpoint and your permission checks. Defaults to api |
X-Tether-User-Session | tus_… | A user session: acts for the user Tether signed in, in their business, on the approval routes only. The user headers are not read with it |
X-Client-Surface | web | mobile | Which of your surfaces is asking. Affects retrieval and telemetry only |
All four are optional. Values that are not positive integers are treated as absent rather than
rejected, so a stray X-Tether-Scope-Id: null scopes the turn to the whole tenant instead of
failing loudly.
Only a secret key may set them. They are read from a tth_ request and ignored on every
other kind. An agent key and a publishable key speak for the tenant and for nobody in
particular, and an action token speaks for the approver it was minted with — see below.
curl -X POST https://api.trytether.ge/v1/chat \
-H "X-Api-Key: $TETHER_SECRET_KEY" \
-H "X-Tether-Scope-Id: 9" \
-H "X-Tether-User-Id: 431" \
-H "X-Tether-User-Role: shift_lead" \
-H "Content-Type: application/json" \
-d '{ "prompt": "what is still open?" }'
Authenticated versus asserted
Approving with an action token
An action token proves which write may be confirmed. On its own it names nobody, so under any registered approval policy it confirms nothing, and where no policy is registered a confirm that names nobody is refused rather than executed.
To let a token approve, bind the approver when you stage the write:
curl -X POST https://api.trytether.ge/v1/actions \
-H "X-Api-Key: $TETHER_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "tool": "refund_order", "arguments": { "id": 4412 }, "approverUserId": 431 }'
The returned confirmation_token then speaks for user 431 and for no one else. The identity
travels with the token, in Tether's database, so whoever holds the token cannot choose a
different approver — and cannot satisfy a two-person policy alone by naming two.
Bind the approver only after your own system has authenticated them. Tether takes the same view
of approverUserId as of X-Tether-User-Id: your backend vouched for it.
Tokens that travel with a turn
Two more headers may come with a request from any kind of key. Neither authenticates the caller to Tether, and neither widens what the key may reach.
| Header | What Tether does with it |
|---|---|
X-Tether-Customer-Token | Verifies it against your customer-token key and passes the customer to your API. Refused 401 invalid_customer_token when it does not verify. See Verified customers |
X-Tether-Upstream-Token | Checks its shape only, and sends it to your imported API as Authorization: Bearer … when your userToken mode says so. Never stored or logged. Refused 400 invalid_upstream_token when malformed. See Calling your API as the signed-in user |
Never call Tether from a browser
The API key is a bearer credential for your whole tenant: it can read your catalog, replace it, stage writes, and spend your budget. Anything holding it can do all of that.
The intended topology is one hop:
browser ──▶ your backend ──▶ Tether
(holds the key, sets scope and user from the session)
Scope never comes from the body
A request body carrying organizationId or locationId is accepted and those fields are
ignored. They are legacy and they are not read. See
Tenants and scope.
The signing secret is a different credential
Your tenant also has a signing secret, and it is not the API key:
| Direction | Purpose | |
|---|---|---|
| API key | You → Tether | Proves which tenant is calling |
| Signing secret | Tether → you | Proves a tool call really came from Tether |
Mixing them up is the most common cause of a signature mismatch. See Verifying signatures.