Tether
DocsGitHub

SDKs

SDKs

The tool endpoint is a signed POST and a JSON body, so you can write one by hand in any language — the contract pages exist so you can. These libraries exist because four parts of it are easy to get subtly, silently wrong.

What they do

Nodesdk/node — plus the tether CLI
Pythonsdk/python — 3.8+, standard library only
.NETsdk/dotnet — net8.0, no package references

Each one owns the same four refusals, with the same error codes:

What arrivesWhat happens
A body that does not match its signature401 invalid_signature, compared in constant time
A timestamp outside the 300s tolerance401 stale_timestamp — a captured request cannot be replayed later
A write that did not arrive in execute mode400 execute_mode_required, and your function does not run
A request_id you have already answeredthe first result, X-Tether-Replayed: true, your function not re-run

Plus one thing that is not a refusal: build_catalog derives the catalog you register from the same definitions the endpoint dispatches, so the schema Tether holds cannot drift from the function that serves it.

They do not decide what a write is

Every SDK reads the write flag from an explicit declaration and from nothing else. Not the tool name, not the HTTP verb, not a heuristic.

// Node. This is a read. It will be dispatched inline, without a confirmation.
delete_everything: {
  description: 'Reads a report and changes nothing at all.',
  handler: () => report(),
}

That looks alarming and it is the point. delete_everything is a read until you say otherwise, because the alternative — a library guessing from a name — is a library that guesses wrong on someone's archive_customer and executes it inline. You declare it; nothing infers it.

Why they are not the same shape

SDKEntry point
NodecreateHandler({ secret, tools }) returns an express (req, res)
PythonTether(...).handle(headers, raw_body) returns a Response
.NETnew TetherEndpoint(...).HandleAsync(headers, rawBody) returns a ToolResponse

Node returns a handler because express is what a Node service runs, and returning anything else would mean a customer writing glue for a framework the SDK could have targeted.

Python and .NET return the answer rather than serving it, because neither ecosystem has a default worth binding the contract to. FastAPI, Flask, Django, a minimal API and a controller are all three lines from a function that takes headers and a body — and none of them would have been three lines from a handler shaped for one of the others.

Idempotency is per-process by default

Each SDK ships an in-memory store for replayed requests. It forgets on restart and knows nothing about your other instances, so behind a load balancer a replay reaches a process that never saw the original — and runs your function a second time.

For a read that is a wasted query. For a write it is a duplicate write. Every SDK takes a store you supply; back it with Redis or your database before a write tool goes live.

One contract, four implementations

The engine, and all three SDKs, read the same file: header names, worked signature vectors, and the exact validation messages.

Before it existed, each implementation was tested against itself. Renaming a header in the engine would leave every test suite green and break every endpoint built on every SDK, and the only thing standing between that and production was somebody remembering to re-read four files at once.

Now a change in one fails in the other three. The vectors deliberately include an empty body and non-ASCII in both the body and the secret, because UTF-8 handling is where independent implementations actually drift apart.

Which to use

Whichever language your service is already written in. There is no capability difference between them: the same four refusals, the same codes, the same catalog output.

The tether CLI lives in the Node package and stays there — lint, register, try, import and actions all speak the same HTTP API, so it works against a Python or .NET endpoint exactly as well. A second CLI would be a second place for the contract to drift.

If your language is not here, the tool endpoint contract is six pages and a worked example. It is genuinely writable by hand — read Verifying signatures first, and Retries, timeouts, idempotency second.