# Calling a Decision

Call a deployed Decision over HTTP, from the SDK or from the CLI, with your own TypeSafe key or TypeFuse's.

## What you need

- A deployed release in an environment; see [Deploying, environments and approvals](https://typefuse.dev/guides/deployments).
- A runtime key for that environment, from **API keys** → **Runtime keys**. Keys for a production
  environment start `tf_live_`, the others `tf_test_`. The key decides which environment answers.
  Each key belongs to an Application, so every call in the [Decision log](https://typefuse.dev/guides/decision-log) says
  which of your systems made it. A runtime key can only call Decisions; it can't edit, publish or
  deploy. Making runtime keys needs a plan that includes production.
- For a BYOK or Relay environment, your own key for TypeSafe, the AI model provider that answers
  every Decision.

## BYOK, Managed and Relay

Each environment says how callers reach TypeSafe (Environments → **How callers reach TypeSafe**):

- **BYOK** (bring your own key) is the default for new production environments. Each call carries
  your TypeSafe key, which is used for that call only and never stored. A call without one is
  refused with `400 TYPESAFE_KEY_REQUIRED`. A BYOK environment also accepts relayed calls.
- **Relay** is for a TypeSafe key that must never leave your own systems, because a policy, a
  contract or a regulator says so. TypeFuse prepares the exact request, your app sends it to
  TypeSafe with its own key and hands the response back, and TypeFuse checks it, records the run and
  answers as usual. Calls are a little slower, shadow releases don't run, replays of production leave
  these calls out, and the record marks each response as reported by your app. A Relay environment
  accepts only relayed calls; a plain call is refused with `409 RELAY_REQUIRED`.
- **Managed** means TypeFuse's own TypeSafe key answers, and its usage is billed. Environments that
  aren't production, such as Test, start as Managed, and so does production on Enterprise, where
  Managed is part of the contract. Relayed calls are refused with `409 ROUTE_NOT_ENABLED`.

Choose BYOK unless your key must never leave your systems: it is the simpler integration and the
faster call. In every case the request is validated, the release chosen and the answer recorded on
TypeFuse's servers, and the answer has the same shape.

## HTTP

```sh
curl https://run.typefuse.dev/v1/acme/refunds \
  -H "Authorization: Bearer $TYPEFUSE_KEY" \
  -H "X-TypeSafe-Key: $TYPESAFE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"input": {"request": "Arrived damaged", "days_since_delivery": 12}, "accepts": "^1.0.0"}'
```

Leave out `X-TypeSafe-Key` for a Managed environment. The body takes:

- `input`: the state your Definition's `state_schema` describes
- `accepts`: the releases you accept, such as `^1.0.0` (see [Versions and releases](https://typefuse.dev/guides/versions#accepting-a-range-of-releases)), or
  `version`, an exact revision, but not both. TypeFuse never falls back to another major line on its
  own: a caller that accepts `^1` gets a 1.x release or `409 INCOMPATIBLE_VERSION`, never 2.x
- `subject` (optional): a stable id for the thing being decided, so a canary gives it the same
  release every time
- `reference` (optional): `{ "type": "order", "id": "A-1042" }`, your own record this call is about
- `correlationId` (optional): an id that ties the call to the rest of your request; the
  `X-Correlation-Id` header works too

The answer:

```json
{
  "runId": "run_…",
  "decision": "refunds",
  "identifier": "acme/refunds",
  "environment": "production",
  "version": 4,
  "semanticVersion": "1.2.0",
  "role": "primary",
  "answers": { "eligible": { "type": "noul", "noul": 0.94 } },
  "latencyMs": 412
}
```

`GET https://run.typefuse.dev/v1/acme/refunds` with the same key describes what is deployed: the releases, the
state schema and the questions.

Errors come back as `{ "code", "message" }`. The ones to handle:

| Status | Code | Means |
| --- | --- | --- |
| 400 | `INVALID_INPUT` | The input doesn't fit the state schema |
| 400 | `TYPESAFE_KEY_REQUIRED` / `TYPESAFE_KEY_NOT_USED` | A BYOK call without a key, or a Managed call with one |
| 401 | `UNAUTHORIZED` | The runtime key is missing or wrong |
| 402 | `PLAN_LIMIT` | Your plan doesn't serve this call; see [Plans and usage](https://typefuse.dev/guides/plans) |
| 404 | `NOT_DEPLOYED` / `VERSION_NOT_DEPLOYED` | Nothing, or not that revision, is deployed in the key's environment |
| 409 | `INCOMPATIBLE_VERSION` | No deployed release is in the range you accept |
| 429 | `RATE_LIMITED` | Your organisation has made its plan's calls for this minute, across all its keys; wait for `Retry-After` |
| 502 | `PROVIDER_ERROR` | TypeSafe couldn't answer; the call is still recorded |
| 503 | `NOT_RECORDED` | With "Record before answering" on: the call couldn't be recorded, so no answer was sent; try again |

Every call is recorded. Your answer is sent as soon as the model replies, and its record follows
within seconds; if storing it fails, TypeFuse keeps retrying from a second copy. For work that must
never give an answer without its record, owners and admins can turn on **Record before answering**
(Environments → Recording calls): each call then waits for its record, about a tenth of a second
more, and you get `503 NOT_RECORDED` instead of an answer if it can't be kept.

## SDK

`@typefuse/sdk` is a small client for the same HTTP API. It is not on npm yet; it will be published
with the move to the `typefuse.ai` domain.

```ts
import { createRuntime } from "@typefuse/sdk";

const typefuse = createRuntime({
  apiKey: env.TYPEFUSE_KEY,
  typesafeKey: env.TYPESAFE_KEY, // leave out for a Managed environment
});

const { answers } = await typefuse.decide(
  "acme/refunds",
  { request: "Arrived damaged", days_since_delivery: 12 },
  { accepts: "^1.0.0", reference: { type: "order", id: "A-1042" } },
);
```

`accepts` can also be a list, such as `["^2.0.0", "^1.0.0"]`, tried in order. That is your code
saying it accepts both contracts, so use it only when your code handles both answer shapes: a new
major can change the input, the questions or the answers. The result's `version` and
`semanticVersion` say which release answered; check them before reading the answers.

The SDK retries briefly when the runtime is busy or unavailable, and turns error codes into typed
errors such as `InvalidInputError`, `IncompatibleVersionError` and `PlanLimitError`. `noul()`,
`choice()` and `score()` read an answer and check its type, and `perItem()` pairs
[per-item answers](https://typefuse.dev/guides/per-item-questions) with their items.

For a Relay environment, add `route: "relay"`. The SDK then sends your TypeSafe key only to
TypeSafe, never to TypeFuse:

```ts
const typefuse = createRuntime({
  apiKey: env.TYPEFUSE_KEY,
  typesafeKey: env.TYPESAFE_KEY,
  route: "relay",
});

const { answers } = await typefuse.decide("acme/refunds", input, { accepts: "^1.0.0" });
await typefuse.flush(); // before your process exits
```

`flush()` waits for any completion still being retried in the background.

## Relay without the SDK

A relayed call is three requests. Never send `X-TypeSafe-Key` to TypeFuse on Relay.

1. `POST https://run.typefuse.dev/v1/acme/refunds/prepare` with your runtime key and the ordinary call body. It
   answers with a `token`, the release that will answer, and `call`: the `url`, `method`, `headers`
   and `body` to send to TypeSafe. When there is nothing to ask (every question is per item and every
   list is empty), `call` is `null` and the answers are already there.
2. Send `call` to TypeSafe with `Authorization: Bearer <your TypeSafe key>`, passing `call.body`
   exactly as given, without parsing or re-serialising it.
3. `POST https://run.typefuse.dev/v1/acme/refunds/complete` with `{ "token": …, "response": { "status", "body",
   "requestId", "sentAt", "receivedAt" } }`, where `body` is TypeSafe's response text as received
   and `requestId` its `x-typesafe-request-id` header. If TypeSafe couldn't be reached, send
   `{ "token": …, "response": { "error": "network", "message": … } }`. It answers in the ordinary
   shape.

The token can be used once and lasts 10 minutes; repeating the same completion is safe and gets the
same answer. A call counts once, when it is prepared, and a prepared call that is never completed
shows in the Decision log as abandoned. A response that doesn't fit the request is refused with
`502 PROVIDER_ERROR` and recorded as an error.

## CLI

The `typefuse` command calls a Decision the same way. Like the SDK, it isn't on npm yet.

```sh
typefuse call acme/refunds --key $TYPEFUSE_KEY --typesafe-key $TYPESAFE_KEY \
  --input '{"request": "Arrived damaged", "days_since_delivery": 12}' --accepts ^1.0.0
```

`--reference order:A-1042`, `--correlation` and `--subject` match the HTTP fields.

## MCP

Agents connected over MCP build, test, publish and deploy Decisions (see
[Connecting an agent](https://typefuse.dev/guides/agents)). They don't call deployed Decisions through MCP: your software
calls the runtime, as above. The full list of control-plane endpoints is in the
[API reference](https://api.typefuse.dev/docs).
