Skip to content
All guides

Guides · Ship

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.
  • 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 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

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), 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:

{
  "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:

StatusCodeMeans
400INVALID_INPUTThe input doesn't fit the state schema
400TYPESAFE_KEY_REQUIRED / TYPESAFE_KEY_NOT_USEDA BYOK call without a key, or a Managed call with one
401UNAUTHORIZEDThe runtime key is missing or wrong
402PLAN_LIMITYour plan doesn't serve this call; see Plans and usage
404NOT_DEPLOYED / VERSION_NOT_DEPLOYEDNothing, or not that revision, is deployed in the key's environment
409INCOMPATIBLE_VERSIONNo deployed release is in the range you accept
429RATE_LIMITEDYour organisation has made its plan's calls for this minute, across all its keys; wait for Retry-After
502PROVIDER_ERRORTypeSafe couldn't answer; the call is still recorded
503NOT_RECORDEDWith "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.

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 with their items.

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

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.

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). 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.