Tether
DocsGitHub

The tool endpoint

The tool endpoint

Tether reaches your data through exactly one HTTPS endpoint that you write, host, and control. There is no connector, no database user, no read replica, and no agent running inside your network.

Why there is only one

Every tool in your catalog is posted to the same URL. The tool name is a field in the body, not a path segment.

Notifications about staged writes, such as action.pending, arrive at the same URL. They are not tool calls, and your handler must tell the two apart; see Lifecycle events.

That is a deliberate trade. One URL means one place to verify signatures, one place to apply your permission model, one place to log, and one thing to rotate when the signing secret changes. A route per tool means every one of those is a checklist item you can forget on the sixty-first tool.

Tether ──POST──▶ https://yourapp.com/tether/invoke
                   │
                   ├── verify signature
                   ├── scope by tenant
                   ├── switch on tool name
                   └── return JSON

The contract in four sentences

  1. Tether POSTs a JSON envelope, signed with your secret over the raw body.
  2. You verify the signature before parsing anything.
  3. You scope every query by the tenant in the envelope, never by anything in the arguments.
  4. You return JSON. Whatever you return is what the model sees.

A complete minimal implementation

import express from 'express'
import crypto from 'node:crypto'

const app = express()
// Raw body — the signature covers the exact bytes, not a re-serialized object.
app.use('/tether/invoke', express.raw({ type: 'application/json' }))

app.post('/tether/invoke', async (req, res) => {
  if (!verify(req)) return res.status(401).json({ error: 'bad signature' })

  const { tool, arguments: args, tenant, is_write, mode } = JSON.parse(req.body)

  try {
    switch (tool) {
      case 'get_open_tickets':
        return res.json({ tickets: await listOpenTickets(tenant, args.limit ?? 20) })

      case 'void_ticket':
        // mode === 'execute' — this only arrives after a human confirmed it.
        return res.json({ voided: await voidTicket(tenant, args.ticket_id, req.get('X-Tether-Idempotency-Key')) })

      default:
        return res.status(400).json({ error: `unknown tool ${tool}` })
    }
  } catch (err) {
    // A tool failure is data, not an outage. Say what went wrong.
    return res.json({ error: err.message })
  }
})

That is the entire integration. Everything else on these pages is detail about the four sentences above.

What Tether never has

Your database credentialsNever sent, never stored, never requested
A network path into your infrastructureOutbound HTTPS to your public endpoint only
Any tool you did not registerThe catalog is the complete set of callable names
The ability to write without a confirmationWrites are staged; your endpoint hears about them after a person confirms

What you own

The permission model. Tether tells you which tenant, scope and user is asking. It does not know that shift leads may void under $50. Your endpoint is the authority on that.

The data shape. Whatever you return enters the model's context verbatim. Returning a full customer record because it was convenient means the model has seen every field in it.

Idempotency. Confirmed writes carry a stable key and are never retried by Tether. Making the operation safe to repeat is your side of the contract, because your side is the only one that can check whether it already landed.

Where to go next

  1. Read the envelope. Every field that arrives, and which of them you may trust — Request envelope.

  2. Verify the signature. With working code in four languages — Verifying signatures.

  3. Return a result. Shape, size ceiling, and how to report a failure — Returning results.

  4. Handle repetition. Which calls retry, which never do — Retries, timeouts, idempotency.