Integration guide
Your application keeps the official OpenAI or Anthropic SDK it already uses. Change two things, the base URL and the API key, and every request is governed: identified, classified, policy-checked, routed or denied, budgeted and evidenced. Optional headers tell TerraFlow more about a request; none is required to start.
The preview is available 10:00–22:00 IST (04:30–16:30 UTC). Outside those hours the server is off and an SDK call fails to connect (a connection error, not an HTTP response).
Your base URL and key
- Base URL. Each workspace has its own host,
https://<your-workspace-host>. The console's Connect panel shows yours, with snippets ready to copy. The OpenAI SDKs take the base URL with/v1on the end; the Anthropic SDKs take the host alone. - API key. It starts with
tf_. The workspace has one key. It is shown once, when the workspace admin redeems the admin invite; colleagues who redeem their own invites get no key. A workspace admin can rotate it, which shows the new key once and revokes the previous one at once. TerraFlow stores only a SHA-256 hash of it; the console shows its first characters, never the key. Send it as the SDK does:Authorization: Bearer tf_…orx-api-key. Keep it in an environment variable, as the snippets do; never put it in code you share. - Choosing the workload.
X-TerraFlow-Workloadnames it. Without that header, amodelthat is a workload id (for examplestarter-assistant) chooses it; otherwise your workspace's default workload,starter-assistant, applies. Only an unknownX-TerraFlow-Workloadis refused (UNKNOWN_WORKLOAD, 403). - Pinning a model. A
modelthat is a chat model's TerraFlow id (such asanthropic-sonnet) or its vendor model id (such asclaude-sonnet-5-5) pins that model. The request is still policy-checked, and if that model is not permitted the request is denied with the reason, not sent elsewhere. Any othermodelvalue, including a name TerraFlow does not know andauto, pins nothing: TerraFlow chooses a model within the workload's contract. A typo in a model name is therefore routed, not refused; checkx-terraflow-capabilityto see which model answered.
Starter workloads
| Workload | What it is for | Data it accepts | Notes |
|---|---|---|---|
starter-assistant |
General questions | public, internal | Routed across Claude, GPT and Gemini models; exact and semantic cache on |
starter-extraction |
Structured extraction | public, internal | The answer must match a JSON schema (vendor, amount, currency, due date), so a stream is held back until the answer is checked; exact cache on |
starter-knowledge |
Questions over a synthetic public handbook | public, internal | Retrieval from the handbook, embedded on the server; answers must cite their sources |
starter-confidential |
Shows where confidential data goes | internal, confidential | External egress off: only the simulated private model can answer |
starter-budget |
Shows a hard budget stop | public, internal | A tiny hourly budget: after the first call in an hour, the next is denied BUDGET_EXHAUSTED |
Each starter workload accepts up to 30 requests a minute. You can author your own workloads in the console; a colleague must approve a contract you wrote before it runs.
Python
OpenAI SDK
# pip install openai
import os
from openai import OpenAI
client = OpenAI(base_url="https://<your-workspace-host>/v1", api_key=os.environ["TERRAFLOW_API_KEY"]) # your 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)
Anthropic SDK
# pip install anthropic
import os
import anthropic
client = anthropic.Anthropic(base_url="https://<your-workspace-host>", api_key=os.environ["TERRAFLOW_API_KEY"]) # starts with tf_
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)
TypeScript
OpenAI SDK
// npm install openai
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://<your-workspace-host>/v1", apiKey: process.env.TERRAFLOW_API_KEY }); // starts with tf_
const reply = await client.chat.completions.create(
{ model: "starter-assistant", messages: [{ role: "user", content: "In two sentences, what does an AI control plane do?" }] },
{ headers: { "X-TerraFlow-Data-Class": "internal" } },
);
console.log(reply.choices[0].message.content);
Anthropic SDK
// npm install @anthropic-ai/sdk
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "https://<your-workspace-host>", apiKey: process.env.TERRAFLOW_API_KEY }); // starts with tf_
const reply = await client.messages.create(
{ model: "starter-assistant", max_tokens: 300, messages: [{ role: "user", content: "In two sentences, what does an AI control plane do?" }] },
{ headers: { "X-TerraFlow-Data-Class": "internal" } },
);
console.log(reply.content[0].type === "text" ? reply.content[0].text : "");
Endpoints
| Endpoint | Speaks | Use it for |
|---|---|---|
POST /v1/chat/completions |
OpenAI Chat Completions: messages, stream, tools, response_format, max_tokens or max_completion_tokens, temperature, user |
The OpenAI SDKs and anything OpenAI-compatible |
POST /v1/messages |
Anthropic Messages: system, messages, tools, stream, max_tokens, temperature, metadata.user_id |
The Anthropic SDKs |
POST /v1/invocations |
TerraFlow's native API, with its options in the body (see below) | When you want the whole decision back in the response body |
GET /v1/models |
OpenAI model list | Lists your workloads and the chat models in your workspace's catalogue |
GET /v1/requests/{id}/evidence |
JSON | The evidence events of one of your own requests |
DELETE /v1/requests/{id} |
JSON | Cancel one of your requests that is still queued or running |
POST /v1/requests/{id}/feedback |
JSON: outcome (accepted, rejected or reworked), optional rating and reason_code; no free text |
Tell TerraFlow whether an answer was useful; the feedback is evidenced |
GET /v1/reviews/{id} |
JSON | Fetch an answer that was held for human review, once a reviewer released it |
Every governed answer on /v1/chat/completions and /v1/messages also carries a terraflow block in its body:
request_id, outcome, reason_code, capability_id, cost_usd, savings_usd and review_id.
Request headers on /v1/chat/completions and /v1/messages
All are optional. An unknown data class or sensitive type, or a deadline that is not a positive whole number, is
refused with a 400 before anything is governed, so a typo cannot silently lower a protection. An unknown
X-TerraFlow-Workload is denied UNKNOWN_WORKLOAD (403).
| Header | Value | What it does |
|---|---|---|
X-TerraFlow-Workload |
A workload id | Chooses the workload (its contract). Without it, a model that is a workload id chooses it; otherwise your workspace's default, starter-assistant, applies |
X-TerraFlow-Data-Class |
public, internal, confidential or restricted |
Declares the data's class. TerraFlow also scans the request and uses whichever is more sensitive. Without it, the request is treated as the most sensitive class the workload accepts |
X-TerraFlow-Sensitive-Types |
A comma list of pii, pci, phi, credentials |
Declares sensitive content. Added to what the scan detects: you can raise the sensitivity, never hide a detection |
X-TerraFlow-User |
A username in your workspace | The end user a request is made for, for entitlements and evidence; without it, the body's user (OpenAI) or metadata.user_id (Anthropic). Honoured only for an application allowed to assert users; your workspace's application is not, so in the preview it has no effect |
X-TerraFlow-Category |
A category name from the workload's contract | Selects the model tier for workloads that tier by category |
X-TerraFlow-Deadline-Ms |
A positive whole number of milliseconds | How long you are willing to wait. A request that cannot finish in time ends DEADLINE_EXPIRED (504). Starter workloads default to 60000 |
Idempotency-Key |
Your own unique string | Makes a retry safe: see idempotency |
X-Correlation-Id |
Up to 256 visible ASCII characters | Your own id for the request. Recorded in the evidence and echoed back |
traceparent |
A W3C trace context | Recorded in the evidence and echoed back |
Never put real names, e-mail addresses or customer identifiers in any header or in a prompt: this is a preview for synthetic data only.
/v1/invocations
The native endpoint takes its options as body fields, not headers. Only X-TerraFlow-Sensitive-Types,
X-Correlation-Id and traceparent are also read from headers (a correlation_id or traceparent in the body wins).
Every other X-TerraFlow-* header and Idempotency-Key are ignored here. An unknown body field is refused with
INVALID_REQUEST (400).
| Body field | Meaning |
|---|---|
messages |
Required: the conversation, in the OpenAI message shape |
workload |
The workload id; without it, your workspace's default workload |
data_class |
public, internal, confidential or restricted, as X-TerraFlow-Data-Class |
user |
As X-TerraFlow-User (no effect in the preview) |
model |
Pins a model, as described above |
max_tokens, temperature |
As in the OpenAI API |
tools |
OpenAI-shaped function definitions; each must be on the contract's tool list |
deadline_ms |
As X-TerraFlow-Deadline-Ms |
idempotency_key |
As Idempotency-Key |
category |
As X-TerraFlow-Category |
correlation_id, traceparent |
As the headers of the same names |
It returns, at the top level of the body and with the HTTP status of the outcome (denials included): request_id,
outcome, reason_code, text, capability_id, model, tool_calls, finish_reason, usage, cost_usd,
baseline_cost_usd, savings_usd, timing (total_ms, queue_ms, overhead_ms), decision, validation,
evidence_seq, sources and review_id. It does not stream.
cost_usd is what the request cost at list price for its real usage. baseline_cost_usd is an estimate: what
the same request would have cost on the workload's baseline model at list price, a counterfactual that was never run.
savings_usd is the difference between the two.
Idempotency
The same Idempotency-Key with the same body, within 24 hours, returns the original outcome without running or
charging again, marked x-terraflow-idempotent-replay: true. The same key with a different body is
IDEMPOTENCY_CONFLICT (409). In the preview, answers are kept for replay in memory only, and that memory is cleared
when the server stops each night: replaying an answered request on a later day returns
IDEMPOTENCY_REPLAY_NO_CONTENT (409), with the original request id. A refused or failed original replays as itself.
Response headers
Responses to /v1/chat/completions, /v1/messages and /v1/invocations that TerraFlow governed, answered or
refused, carry these headers, except a stream that passes straight through (see streaming). Errors
raised before a request is governed (a missing or wrong key, a malformed body, an invalid header value) carry only
x-terraflow-request-id, x-terraflow-correlation-id and traceparent once a request id exists, and their code in
the error body.
| Header | Meaning |
|---|---|
x-terraflow-request-id |
TerraFlow's id for the request. Quote it to find the request in the console and its evidence |
x-terraflow-correlation-id |
Your X-Correlation-Id, or the request id when you sent none |
x-terraflow-outcome |
EXECUTED (a cache hit included), HELD, DENIED, FAILED, EXPIRED, CANCELLED or SHED |
x-terraflow-reason |
The reason code, for example PERMITTED, CACHE_HIT or DATA_CLASS_NOT_PERMITTED |
x-terraflow-capability |
The TerraFlow id of the model that answered, empty when none did |
x-terraflow-cost-usd |
What the request cost at list price for its real usage, in USD |
x-terraflow-savings-usd |
The estimated list-price cost of the same request on the workload's baseline model, minus the real cost. The baseline was never run: this is a per-request estimate, not a claim about your traffic |
x-terraflow-overhead-ms |
Time TerraFlow itself added, in milliseconds |
x-terraflow-cache |
hit or miss |
x-terraflow-idempotent-replay |
true on a replayed outcome |
x-terraflow-review-id |
On an answer held for human review: the id to fetch it with once released |
traceparent |
Your trace context, echoed when you sent one |
retry-after |
Seconds to wait, on refusals that a later retry can pass (rate limits, a shed queue, the monthly ceiling) |
x-should-retry |
false when an automatic retry cannot succeed soon (a spent budget or allowance, an exhausted deadline) |
The official SDKs retry 409, 429 and 5xx responses on their own, and each retry is a new governed request. They honour
x-should-retry: false. If you prefer to handle retries yourself, set the SDK's max_retries (Python) or
maxRetries (TypeScript) to 0.
Errors
A refusal is an ordinary HTTP error in the shape your SDK expects, with TerraFlow's reason code inside it, so your SDK raises its usual exception.
On /v1/chat/completions (OpenAI shape):
{"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"}}}
On /v1/messages (Anthropic shape):
{"type": "error",
"error": {"type": "permission_error", "message": "TerraFlow denied: DATA_CLASS_NOT_PERMITTED"},
"terraflow": {"request_id": "req_01m3j…", "reason_code": "DATA_CLASS_NOT_PERMITTED"}}
/v1/invocations returns its full decision body with the HTTP status of the outcome, denials included. Errors raised
before a request exists (a missing or wrong key, a malformed body) use the OpenAI shape there.
The common statuses: 400 a malformed request or one the provider rejected; 401 a missing or wrong key; 403 refused by policy, data class or contract; 409 an idempotency conflict; 413 a request too large to scan; 422 an answer blocked by validation; 429 a budget, allowance, rate limit or full queue; 503 no model could answer after retries and fallback, or the spend guard, the evidence store or the policy bundle is unavailable; 504 the deadline passed. Every code, its status and what to do about it is in the reason-code reference.
Streaming
Set stream: true as usual; both channels stream server-sent events in their own format.
- A stream is held back, checked whole, and only then sent to you as a stream, so nothing is released before it
passes, when the workload is for a customer or public audience, is at consequence tier T2 or above, has an output
schema, a cascade or content rules, or may hold answers for human review. Of the starter workloads,
starter-extractionis held back, because its answer must match a JSON schema. - Otherwise the model's tokens pass straight through as they arrive:
starter-assistant,starter-knowledge,starter-confidentialandstarter-budget. The answer is checked once it is complete; a failed check is recorded and flagged, but text already sent cannot be recalled. - A streamed response starts with
x-terraflow-request-idandx-terraflow-correlation-id. On a pass-through stream the outcome, cost and other headers are known only at the end: read them in the console or from the request's evidence (the OpenAI channel's last event also carries the token usage). A held-back stream sends every header first, because its answer is complete before it starts. - A request refused before it starts is an ordinary error response, not a stream. A failure part-way through ends the
stream with an error event in the channel's format (
data: {"error": …}on the OpenAI channel,event: erroron the Anthropic channel).
Tools
Tool definitions are accepted only when every function they name is on the workload's contract. The starter workloads
allow no tools, so a request that offers one is denied TOOL_NOT_PERMITTED (403). On a contract that allows tools,
the model's tool calls come back to your code unexecuted, after the same sensitive-data checks as text; TerraFlow
never runs them for you.
Not supported in the preview
- Embeddings endpoint. There is no public
/v1/embeddings. The on-server embedding model serves TerraFlow's own semantic cache and retrieval. - The OpenAI Responses API (
/v1/responses), the Assistants API, audio, image generation and files. Use Chat Completions or Messages. - Images, audio and files in a prompt. TerraFlow cannot scan them, so it treats them as restricted data containing
every sensitive type. In the preview that request is denied (
DATA_CLASS_NOT_PERMITTED); nothing is sent. - Real data of any kind. See what this preview is.