API

Headers and streaming frames

The headers Sentisec reads and adds, how requests are stitched into sessions, and the trailing verdict frame on streamed responses.

Sentisec adds as little to the wire as it can. One header is required, two are optional, and one comes back on the response. Everything else on a request is forwarded to the upstream untouched.

Request headers

HeaderRequiredWhat it does
Authorization: Bearer sk_sentisec_…yesYour workspace API key. Anthropic-shaped clients may send x-api-key instead.
X-Sentisec-Upstream-ProvidernoPins the request to one upstream, overriding the model-id heuristic. This is how you choose a provider the model id would not select. Stripped before the body is forwarded.
x-session-idnoGroups related requests into one session. See below.

An unrecognised value in X-Sentisec-Upstream-Provider returns 400 upstream_provider_header_invalid; a valid name with no connected credential returns 503 provider_unavailable. See Errors and limits.

Response headers

HeaderWhenWhat it means
X-Sentisec-VerdictOn non-streamed chat-style responses — see when it is absentThe turn's worst recorded verdict (NOMINAL, ELEVATED, SUSPICIOUS, HIJACK); a turn with no inspected actions can report NOMINAL.
X-Sentisec-Telemetry: degradedOnly when setRecording is running behind. Your request was still served and inspected normally.
Warning
Do not branch on `X-Sentisec-Verdict`

A verdict does not tell you whether its intervention was applied: shadow mode can record the same verdict without a policy block. NOMINAL also does not establish that an action was inspected. Detect a block from the response body: the [Sentisec blocked tool_use: …] text, or the sentisec_tool_call_blocked call if your workspace opted in to rename (shapes here). For the recorded verdict, read the session's verdicts (Sessions) or the dashboard.

When the verdict header is absent

Sentisec stamps this header only where the value would be true of the bytes it rides on. Where it would not be, the header is left off rather than defaulted — so a missing X-Sentisec-Verdict is a statement, not a fault. The request is still forwarded and answered normally and there is nothing to retry. Three situations produce it.

Every streamed model call, from 0.8. Messages, Chat Completions (including calls routed to another provider), and Responses send no X-Sentisec-Verdict header. Headers leave before the completed verdict exists. For these SSE APIs, read the trailing : sentisec-verdict comment instead. A blocked Chat stream has exactly one data: [DONE]; an allowed Bedrock mantle Chat stream ends without [DONE], as the provider sends it.

Native Bedrock ConverseStream uses binary events rather than SSE: its verdict is in messageStop.additionalModelResponseFields.sentisec, also without a verdict header. See Bedrock.

Codex on a ChatGPT subscription. In subscription mode a Codex request is forwarded to OpenAI from your own machine, and the action it proposes is evaluated on its own path rather than on the reply coming back. Nothing in the response path examined these bytes, so nothing marks them. The verdict is still recorded — read it from the session or the dashboard.

A request that could not be matched to a workspace. If the credential on a request does not resolve to a workspace, there is no policy for Sentisec to evaluate the request against, so the verdict is withheld rather than defaulted to NOMINAL. This is the case worth acting on: it means the call was not governed by the workspace you expected. Send your workspace API key against the workspace-scoped URL — /ws/{workspace_id}/v1/… — and it resolves.

Sessions and how requests are stitched together

A session is Sentisec's unit of context: the run of related calls an agent makes while working on one task. Verdicts, the timeline, and forensic evidence are all organised by session.

For a custom agent or BYOK integration, send a unique x-session-id for each workflow run and reuse it for every model request, tool continuation, and retry in that run. Generate the next id when the user starts another run; a provider or model change is not a new run by itself. Do not put a credential or personal information in the id.

Without an explicit id, automatic correlation can join separate runs that share the same workspace key and opening context. Changing models does not reliably separate them. An explicit run id removes that ambiguity. Existing clients that already send a recognised session header keep their own grouping. For example:

curl https://api.sentisec.ch/ws/$SENTISEC_WORKSPACE_ID/v1/messages \
  -H "Authorization: Bearer $SENTISEC_API_KEY" -H "x-session-id: run-2f9c41" -H "content-type: application/json" \
  -d '{"model":"claude-sonnet-4","max_tokens":1024,"messages":[{"role":"user","content":"Hello"}]}'

Sentisec resolves the session in this order:

  1. A session id supplied by the client. x-session-id is the general one; the equivalents that Claude Code and OpenCode already send are recognised automatically, so those tools need no extra configuration.
  2. A W3C traceparent header, if your stack already propagates one.
  3. A fingerprint derived from the calling key and the opening message — the automatic fallback when nothing above is present.

Reuse the same value across a run to keep it in one session; change it to start a new one. Session ids are scoped to your workspace.

Warning
Send `x-session-id`, not `x-sentisec-session-id`

Some Sentisec SDK versions put an x-sentisec-session-id header on the wire. It does not control grouping — the API reads the three headers listed above and nothing else, so a request carrying only x-sentisec-session-id falls through to the automatic fingerprint. If you are setting session ids by hand, set x-session-id.

The trailing frame on a streamed response

An SSE model response has nowhere to put a verdict header — headers go out before the first frame — so the verdict travels at the end of the stream instead, on one extra line:

: sentisec-verdict {"verdict":"NOMINAL"}

It is an SSE comment — the leading : is what makes it one — rather than a data: frame or a named event, so every spec-compliant parser ignores it and adding it cannot break a client that was not written for Sentisec. Treat it as an inline hint: tolerate it, never require it, and never read its absence as "the turn was clean".

The full contract — the exact shape, what the value means, how to read it, and what not to do with it — is on The streaming verdict frame. Which endpoint to stream from is covered on Chat endpoints.

Protected docs