Tether
DocsGitHub

Start here

Your first write

A model that can only read is a search box. A model that can write without asking is a liability. This page is the third option.

The rule

The model never mutates anything. When it calls a tool you declared is_write, Tether does not contact your endpoint. It writes down what the model wants to do, returns a preview, and stops.

Your endpoint sees the write only after a separate, authenticated call confirms it.

The flow

  1. The model proposes. It calls void_ticket with { "ticket_id": "T-4417" }.

  2. Tether stages it. A pending action is persisted with an id, the command, the tenant it belongs to, and an expiry. Nothing is called.

  3. Your user sees it. The turn returns with a pending_actions entry alongside the blocks. You render a confirmation card.

  4. Your user confirms. Your backend POSTs to /v1/actions/{id}/confirm with the secret key, naming the person in X-Tether-User-Id. A browser never holds that key; it confirms with a single-action token instead, see Approving with an action token.

  5. The guard runs. The confirming credential must belong to the tenant the action was staged for, and a write registered elevated or high risk needs an explicit acknowledgement.

  6. The action is claimed. A compare-and-set flips it from pending to executing. Two confirms racing produce one execution and one Replayed.

  7. Now your endpoint is called, with "mode": "execute" and a stable idempotency key.

What comes back on the turn

{
  "content_type": "application/vnd.tether.blocks.v1+json",
  "blocks": [
    { "type": "text", "id": "confirm_prompt", "variant": "body",
      "markdown": "I can void ticket **T-4417**. It is a $84.50 tab opened 41 minutes ago." }
  ],
  "pending_actions": [
    {
      "action_id": "9c1e7b6a-3f42-4a51-9d0e-6b2c8f4a1d33",
      "plan_id": "1a7f…",
      "tool": "void_ticket",
      "command_type": "void_ticket",
      "summary": "void ticket — awaiting confirmation.",
      "expires_at": "2026-08-20T18:41:07Z"
    }
  ]
}

Render a card per entry. The entries carry no credential, and a browser's publishable key cannot confirm them, so the card's button calls your backend. See Building a confirmation UI.

Confirming

curl -X POST https://api.trytether.ge/v1/actions/9c1e7b6a-3f42-4a51-9d0e-6b2c8f4a1d33/confirm \
  -H "X-Api-Key: $TETHER_SECRET_KEY" \
  -H "X-Tether-User-Id: 431" \
  -H "Content-Type: application/json" \
  -d '{ "acknowledgeHighRisk": true }'

void_ticket was registered "risk": "high", so without the acknowledgement the guard refuses:

{
  "status": "GuardRejected",
  "actionId": "9c1e7b6a…",
  "errorCode": "AI_ACTION_REQUIRES_CONFIRMATION",
  "errorMessage": "'void_ticket' is registered as a high-risk write. Re-submit the confirmation with acknowledgeHighRisk = true to proceed."
}

That flag is meant to represent a deliberate second gesture in your UI — a typed confirmation, a held button, a manager PIN. Do not hardcode it to true and call it a confirmation flow.

What your endpoint receives

Only now, and only once:

{
  "tool": "void_ticket",
  "arguments": { "ticket_id": "T-4417" },
  "tenant": { "organization_id": 42, "scope_id": 9, "user_id": 431 },
  "is_write": true,
  "mode": "execute",
  "request_id": "9c1e7b6a3f424a519d0e6b2c8f4a1d33"
}

request_id is the action id, stable across retries of the same confirmation and delivered as X-Tether-Idempotency-Key. Key your own deduplication on it.

What Tether checks, and what it cannot

Tether enforces the tenant boundary, because it is the only party that can: the action was staged against one tenant, and the credential confirming it must be that tenant. Without that check, a valid key for one customer could confirm another customer's staged write.

Tether cannot enforce your permission model. It does not know that shift leads may void under $50 and managers may void anything. Your endpoint sees every write with "is_write": true and can refuse — that is where your rules belong.

Confirmation outcomes

A confirm answers with a status such as Executed, Replayed, GuardRejected or Expired, each with its own HTTP code. Every status, and what to show for it, is in one table on the Actions page, so this page cannot drift from it.

Things worth knowing

Pending actions expire. A stale proposal should not be confirmable an hour later against data that has moved on.

A write is never offered on an image turn. A model should not be asked to stage a mutation from a photograph it may have misread. This is enforced regardless of what the catalog says.

Write tools are off by default in chat. Set enableWriteActions: true on the request to let a conversational turn stage writes.

Next