Guides
Calling your API as the signed-in user
An imported API is called with the one credential you stored at import. Some APIs accept only their own users' session tokens, which expire, and issue no long-lived service credential. For those, the page passes the signed-in person's token with each turn, and Tether calls your API with it. Nothing changes on your server.
The flow
- With the secret key, you choose how your imported API uses a person's token:
ignore,preferorrequire. - Your page, or the widget, sends the signed-in person's session token with every turn in
X-Tether-Upstream-Token. - When Tether calls your imported API during that request, it sends that token as
Authorization: Bearer …instead of the stored credential, as the mode says. - Your API checks the token exactly as it does for your own app, and answers as that person.
1. Choose the mode
| Mode | A token came with the request | No token came with the request |
|---|---|---|
ignore (default) | The stored credential is sent; the token is not used | The stored credential is sent |
prefer | The person's token is sent | The stored credential is sent |
require | The person's token is sent | Your API is not called. The call fails with user_token_required, and the model is told to ask the person to sign in |
Under require Tether never falls back to the stored credential. Every tenant starts at ignore,
so nothing changes until you choose. Set it without importing again:
curl -X PATCH https://api.trytether.ge/v1/tools/api \
-H "X-Api-Key: $TETHER_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "userToken": "require" }'
{ "baseUrl": "https://api.yourcompany.com", "userToken": "require", "credentialStored": false }
Or pass userToken on POST /v1/tools/import, or use
tether import --user-token require and tether api set --user-token require from the
CLI. See
Changing the connection for the whole route.
The mode decides the bearer for every call Tether makes to your imported API: reads, the read behind a write's precondition, the read that verifies a write, and the confirmed write itself.
2. Send the token with each turn
With the widget, as an attribute, or as a property when the page refreshes the session:
<tether-assistant endpoint="…" publishable-key="tpk_…" upstream-token="the person's session token"></tether-assistant>
document.querySelector('tether-assistant').upstreamToken = refreshedToken
From your own client or backend, as a header on /v1/chat,
/v1/chat/stream, POST /v1/actions or POST /v1/tools/{name}/call:
curl -X POST https://api.trytether.ge/v1/chat \
-H "X-Api-Key: $TETHER_PUBLISHABLE_KEY" \
-H "X-Tether-Upstream-Token: $SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "prompt": "what is on my order 4412?" }'
The header is accepted on any authenticated route and with any kind of key. A leading Bearer
is stripped, so you can pass your own Authorization value as it is. The SDKs export the name:
UPSTREAM_TOKEN_HEADER in Node and Python, TetherCustomerToken.UpstreamTokenHeader in .NET.
When a token is refused
Tether refuses a token only for its shape, before anything runs, with 400:
{
"error": {
"code": "invalid_upstream_token",
"reason": "too_long",
"message": "The upstream token is 4100 characters; the limit is 4000."
}
}
| Reason | Means |
|---|---|
empty | The header is present with nothing in it, or nothing after Bearer |
too_long | Longer than 4000 characters |
invalid_characters | Anything but printable ASCII: a space, a control character, a line break or a non-ASCII letter |
repeated | The header was sent more than once |
The message never quotes the value. The widget tells the person their sign-in could not be used
and dispatches tether-upstream-token-refused with detail.reason.
An expired or revoked token is your API's answer, not Tether's. When your API answers 401 or
403, the tool result says which credential was sent: the person's token, which has probably
expired and should be passed fresh, the stored credential, or none. The token itself is never
repeated.
Confirming a write
A write in your imported catalog is staged and confirmed as any other write. It runs inside the confirm request, so it uses the confirming request's token, not the one the turn that staged it carried. Who confirms decides what is sent:
| Who confirms | What happens |
|---|---|
| A person in the approval inbox | The inbox carries no user token. Under prefer the write uses the stored credential. Under require the confirm answers 400 with status FailedTerminal and errorCode user_token_required; your API is not called, the action is settled as failed, and no other credential is tried |
| The widget | Never. The element does not confirm writes |
| Your backend, with the secret key | It may send X-Tether-Upstream-Token on POST /v1/actions/{actionId}/confirm, and that token is used |
So under require, writes go through your backend, confirming on behalf of the person who is
signed in, with their token on the confirm. A write approved in the inbox under require fails
cleanly rather than running as somebody else.
Calling a read directly
POST /v1/tools/{name}/call under require without a token answers 403 with user_token_required
in the body's status, the same way customer_required is reported. Your API is not called.
What it does not change
- Only the imported API. The header reaches nothing but the calls to the base URL of your imported API. Your signed tool endpoint and an MCP upstream ignore it.
- It is not a customer token. Tether cannot read who the person is from your token. To let a capability act for one customer and be checked by Tether, send a verified customer token as well. The two travel side by side.
- It is not a Tether credential. It never authenticates a request to Tether and never widens what a key may reach.
Related
- Tools — import and the connection route
- The widget
- Verified customers
- Errors