TerraFlow

Docs · API guide

Connect an app in two changes

Keep the official OpenAI or Anthropic SDK you already use. Change the base URL and the API key, and every request is governed: identified, classified, checked against its rules, routed or refused, budgeted and recorded.

Base URL and key

Base URL
The address of your TerraFlow plane, https://<your-terraflow-host>. The console's Connect screen shows yours, with snippets ready to copy. OpenAI SDKs take the base URL with /v1 on the end; Anthropic SDKs take the host alone.
API key
Starts with tf_. Send it the way your SDK already does: Authorization: Bearer tf_… or x-api-key: tf_…. Both are accepted on every /v1 endpoint.
Key storage
TerraFlow stores only a SHA-256 hash of the key; the console shows its first characters, never the key. An admin can rotate it, which shows the new key once and revokes the old one at once.

Keep the key in an environment variable, as every example below does. A missing, wrong or revoked key is refused with APP_UNAUTHENTICATED (401).

Choosing a workload

A workload is one AI use case with its own signed contract: the data it may carry, the models it may use, its budgets and whether answers need human approval. Every request runs under exactly one.

  1. X-TerraFlow-Workload: <workload-id> chooses it. An unknown id is refused with UNKNOWN_WORKLOAD (403).
  2. Without that header, a model that is a workload id (for example starter-assistant) chooses it.
  3. Otherwise your default workload applies.

Pinning a model. A model that is a model's TerraFlow id (such as anthropic-sonnet) or its vendor id (such as claude-sonnet-5-5) pins that model. The request is still checked against the rules; if that model is not allowed, the request is refused with the reason, never silently sent elsewhere. Any other value, including auto, lets TerraFlow choose within the contract. Read x-terraflow-capability to see which model answered.

OpenAI-compatible: chat completions

POST /v1/chat/completions accepts the Chat Completions body: messages, stream, tools, response_format, max_tokens or max_completion_tokens, temperature, user.

curl

curl https://<your-terraflow-host>/v1/chat/completions \
  -H "Authorization: Bearer $TERRAFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-TerraFlow-Workload: starter-assistant" \
  -H "X-TerraFlow-Data-Class: internal" \
  -H "Idempotency-Key: 6f1c2a9e-ticket-4821" \
  -d '{"model": "auto",
       "messages": [{"role": "user", "content": "In two sentences, what does an AI control plane do?"}]}'

Python · openai

# pip install openai
import os
from openai import OpenAI

client = OpenAI(base_url="https://<your-terraflow-host>/v1",
                api_key=os.environ["TERRAFLOW_API_KEY"])          # starts with tf_
reply = client.chat.completions.create(
    model="starter-assistant",   # a TerraFlow workload, not a vendor model: TerraFlow picks the route
    messages=[{"role": "user", "content": "In two sentences, what does an AI control plane do?"}],
    extra_headers={"X-TerraFlow-Data-Class": "internal"},
)
print(reply.choices[0].message.content)

Every governed answer also carries a terraflow block in its body: request_id, outcome, reason_code, capability_id, cost_usd, savings_usd and review_id.

Anthropic-compatible: messages

POST /v1/messages accepts the Messages body: system, messages, tools, stream, max_tokens, temperature, metadata.user_id.

curl

curl https://<your-terraflow-host>/v1/messages \
  -H "x-api-key: $TERRAFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-TerraFlow-Data-Class: internal" \
  -d '{"model": "starter-assistant", "max_tokens": 300,
       "messages": [{"role": "user", "content": "In two sentences, what does an AI control plane do?"}]}'

Python · anthropic

# pip install anthropic
import os
import anthropic

client = anthropic.Anthropic(base_url="https://<your-terraflow-host>",   # host only, no /v1
                             api_key=os.environ["TERRAFLOW_API_KEY"])
reply = client.messages.create(
    model="starter-assistant",
    max_tokens=300,
    messages=[{"role": "user", "content": "In two sentences, what does an AI control plane do?"}],
    extra_headers={"X-TerraFlow-Data-Class": "internal"},
)
print(reply.content[0].text)

All endpoints

EndpointUse it for
POST /v1/chat/completionsOpenAI SDKs and anything OpenAI-compatible
POST /v1/messagesAnthropic SDKs
POST /v1/invocationsTerraFlow's native API: options as body fields (workload, data_class, idempotency_key, deadline_ms, …) and the whole decision back in the body, including cost_usd, baseline_cost_usd, savings_usd, timing and evidence_seq. Does not stream.
GET /v1/modelsYour workloads and the chat models in your catalogue
GET /v1/requests/{id}/evidenceThe audit events of one of your own requests
DELETE /v1/requests/{id}Cancel a request that is still queued or running
POST /v1/requests/{id}/feedbackoutcome (accepted, rejected, reworked), optional rating and reason_code; no free text
GET /v1/reviews/{id}Fetch an answer that was held for human approval, once a reviewer released it

Headers

Request headers (all optional)

On /v1/chat/completions and /v1/messages. An invalid value is refused with a 400 before anything is governed, so a typo can never silently lower a protection.

HeaderWhat it does
X-TerraFlow-WorkloadChooses the workload (its contract)
X-TerraFlow-Data-Classpublic, internal, confidential or restricted. TerraFlow also scans the request and uses whichever is more sensitive. Undeclared, the request is treated as the most sensitive class the workload accepts
X-TerraFlow-Sensitive-TypesComma list of pii, pci, phi, credentials. Adds to what the scan detects: you can raise sensitivity, never hide a detection
X-TerraFlow-CategoryA category from the workload's contract, for workloads that pick a model size by category
X-TerraFlow-Deadline-MsHow long you will wait. A request that cannot finish in time ends DEADLINE_EXPIRED (504)
X-TerraFlow-UserThe end user the request is for; honoured only for an app allowed to assert users
Idempotency-KeyMakes a retry safe (see below)
X-Correlation-Id, traceparentYour own ids; recorded in the audit record and echoed back

Response headers

HeaderMeaning
x-terraflow-request-idTerraFlow's id for the request; quote it to find the request and its audit events
x-terraflow-outcomeEXECUTED (cache hits included), HELD, DENIED, FAILED, EXPIRED, CANCELLED or SHED
x-terraflow-reasonThe reason code, for example PERMITTED, CACHE_HIT, DATA_CLASS_NOT_PERMITTED
x-terraflow-capabilityThe model that answered; empty when none did
x-terraflow-cost-usdWhat the request cost at list price for its real usage
x-terraflow-savings-usdEstimated cost of the same request on the workload's baseline model, minus the real cost. A per-request estimate: the baseline was not run
x-terraflow-cachehit or miss
x-terraflow-review-idOn an answer held for human approval: the id to fetch it with
retry-after, x-should-retrySee retries

What a governed refusal looks like

A refusal is an ordinary HTTP error in the shape your SDK expects, with TerraFlow's reason code inside, so your SDK raises its usual exception. A request refused by its rules never reaches a model, and the refusal is still recorded in the audit record.

/v1/chat/completions · 403

{"error": {"type": "permission_error", "code": "DATA_CLASS_NOT_PERMITTED",
           "message": "TerraFlow denied: DATA_CLASS_NOT_PERMITTED",
           "terraflow": {"request_id": "req_01m3j…", "reason_code": "DATA_CLASS_NOT_PERMITTED"}}}

/v1/messages · 403

{"type": "error",
 "error": {"type": "permission_error", "message": "TerraFlow denied: DATA_CLASS_NOT_PERMITTED"},
 "terraflow": {"request_id": "req_01m3j…", "reason_code": "DATA_CLASS_NOT_PERMITTED"}}

The same code is in the x-terraflow-reason header. Common codes:

CodeStatusWhat to do
APP_UNAUTHENTICATED401Send a valid tf_ key; after a rotation, use the new one
UNKNOWN_WORKLOAD403Use a workload id from the console's Workloads screen
DATA_CLASS_NOT_PERMITTED403The workload does not accept this data class: use a workload that does, or remove the data
SENSITIVE_TYPE_NOT_ALLOWED_ON_CAPABILITY403Personal, card or health data, or a credential, that the pinned model may not receive: remove it, or let TerraFlow choose
NO_POLICY_PERMITTED_ROUTE403No allowed model may take this request under the rules
TOOL_NOT_PERMITTED403A tool that is not on the workload's contract
VALIDATION_BLOCKED422The answer failed its checks and was not released
BUDGET_EXHAUSTED429A budget has no room: wait for the period to roll over, or have FinOps raise it. Do not retry now
RATE_LIMITED, QUEUE_SHED429Wait for Retry-After, then retry
EVIDENCE_UNAVAILABLE503The audit record could not be written, so nothing was sent to a model. Retry later
DEADLINE_EXPIRED504Raise X-TerraFlow-Deadline-Ms or shorten the request

Held for approval. When a workload's rules require a person to approve an answer, the outcome is HELD with reason REVIEW_PENDING. On /v1/chat/completions and /v1/messages it is a 200 whose answer is a placeholder naming the review id (so SDKs do not break); on /v1/invocations it is a 202. Fetch the released answer with GET /v1/reviews/{id}.

Every code, its status and the action to take is in the reason-code reference, generated from docs/public/reason-codes.yaml and docs/public/error-codes.yaml, which the build checks against the code.

Idempotency and retry-later

The official SDKs retry 409, 429 and 5xx on their own, and each retry is a new governed request. They honour x-should-retry: false. To handle retries yourself, set max_retries=0 (Python) or maxRetries: 0 (TypeScript), and send an Idempotency-Key so a retry can never run or charge twice.

Streaming

Set stream: true as usual; both channels stream server-sent events in their own format.

Limits

LimitWhat happens
Budgets per workload, business unit and tenant, hourly or monthly, as the contract sets themBUDGET_EXHAUSTED (429)
Per-request cost limit in the contractBUDGET_REQUEST_LIMIT
Throughput cap per workload, in the contractRATE_LIMITED (429) with Retry-After
A model's context window (measured conservatively, in bytes)Routed to a larger model, or CONTEXT_WINDOW_EXCEEDED
Request too large to scan413
Hosted preview edge: 2 MB request body; per client address 10 sign-in, 120 /v1 and 600 other requests a minuteRefused at the edge

Not supported

Source of truth: terraflow/gateway/ (openai_router.py, anthropic_router.py, native_router.py, auth.py) and terraflow/core/reasons.py.