REST API reference

Base URL: https://your-firewall.example.com. All endpoints below are relative to it.

This is a field-level reference. For task-oriented walkthroughs, see Direct API, Transparent proxy, and Detect a compromised agent.

Authentication

Machine-facing endpoints (everything except the dashboard-scoped table at the end of this page) authenticate with a firewall API keysfw_live_xxxxxxxx… or sfw_test_xxxxxxxx…. Send it as:

  • X-SFW-Key: sfw_live_xxxxxxxx… — preferred, especially on the proxy where Authorization carries your upstream provider's key.
  • Authorization: Bearer sfw_live_xxxxxxxx… — accepted anywhere except the proxy, where Authorization is reserved for the provider key.

If both are present, X-SFW-Key wins.

auth.enabled defaults to off on a self-hosted firewall. With auth off, /v1/inspect and every other API-key endpoint run anonymously (single implicit tenant) — no key required. When auth is turned on, auth.allow_anonymous_inspect (default true) keeps unauthenticated calls working unless you disable it; an unrecognized or malformed key always gets 401.

Dashboard-scoped endpoints (keys, per-key/tenant config, metrics, the Clerk webhook) authenticate differently — see the Dashboard-scoped endpoints table.

Conventions

  • Every response carries x-request-id (echoes an inbound X-Request-Id or mints one) and x-processing-time-ms.

  • Request bodies over 256 KB (Content-Length based) are rejected before parsing with 413:

    { "error": "request body too large", "limit_bytes": 262144 }
    
  • All request/response bodies are JSON unless noted.

Core inspection

API-key auth. This is the primary integration surface for calling code that controls its own request lifecycle — see Direct API for the full lifecycle walkthrough.

POST /v1/inspect

Inspect one piece of text for PII and prompt injection / drift, and get back a pseudonymized version plus a verdict.

Request (InspectRequest):

FieldTypeRequiredNotes
input_textstringyesMinimum 1 character.
system_goalstringnoThe LLM's intended goal/system prompt, used as the semantic-drift baseline.
request_idstringnoCaller-supplied correlation id.
session_idstringnoCorrelates turns of one conversation for cross-turn injection-pressure tracking. None = stateless, the default.
metadatadict[string,string]noFreeform, stored with the metrics event.
curl -s -X POST https://your-firewall.example.com/v1/inspect \
  -H "X-SFW-Key: sfw_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "input_text": "Email john.smith@acme.com about the invoice.",
    "system_goal": "You are a customer support agent."
  }'

Response (Verdict):

FieldTypeNotes
action"allow" | "flag" | "block"The recommended decision.
confidencenumber, 0–1Agreement across detection channels, not severity — a clean verdict is 1.0.
issuesarray of IssueEmpty when clean.
sanitized_inputstring | nullPII-pseudonymized text; null if no PII was found.
pii_mappingdict[string,string] | null{pseudonym: original}. Keep this client-side to restore real values later.
processing_time_msnumberServer-side processing time.
dry_runbooleantrue when the firewall is in monitor mode — action is still the real would-be decision, but nothing was enforced.

Each Issue:

FieldTypeNotes
type"pii" | "injection" | "drift"
severity"low" | "medium" | "high" | "critical"May be escalated one rank when ≥2 channels corroborate.
detailstringHuman-readable reason.
span{start, end} | omittedCharacter offsets, raw view only.
viewstring | omittedThe normalized view the issue was found in, e.g. base64-decoded, unicode-fold. Omitted for the raw input.
channelstring | omittedThe detector that fired: heuristic, classifier, llm_judge, semantic_drift, pii, session_pressure, coercion.
{
  "action": "flag",
  "confidence": 0.75,
  "issues": [
    {
      "type": "pii",
      "severity": "low",
      "detail": "EMAIL_ADDRESS detected",
      "span": { "start": 6, "end": 27 },
      "channel": "pii"
    }
  ],
  "sanitized_input": "Email alex.morgan@example.com about the invoice.",
  "pii_mapping": { "alex.morgan@example.com": "john.smith@acme.com" },
  "processing_time_ms": 42.1,
  "dry_run": false
}

Pseudonyms are context-preserving fakes — realistic names, example.com emails, 555-prefixed phone numbers — not bracket placeholders, so the model reasons about them correctly instead of hallucinating.

POST /v1/inspect/response

Inspect an LLM's output for signs an injection succeeded (persona shift, prompt/credential leak, safety-bypass confirmation). Same Verdict shape as above.

Request:

FieldTypeRequired
response_textstringyes
system_goalstringno
curl -s -X POST https://your-firewall.example.com/v1/inspect/response \
  -H "X-SFW-Key: sfw_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"response_text": "<the model reply to check>", "system_goal": "You are a customer support agent."}'

Returns 501 with {"detail": "response anomaly engine not enabled on this firewall"} if the response engine is disabled on this deployment.

POST /v1/inspect/async

Dispatch an inspection and get the verdict via webhook instead of in the response — for callers that don't want to block on the pipeline.

Request:

FieldTypeRequired
input_textstringyes
system_goalstringno
request_idstringno — used as the task id if provided
metadatadict[string,string]no
webhook_urlstringyes

Response: {"task_id": "…", "status": "processing"}

Returns 501 with {"detail": "Async dispatch not configured"} if async dispatch isn't wired up on this deployment.

PII round-trip

Public — no auth required on these three routes.

POST /v1/deanonymize

Restore original values in text that carries pseudonyms from a prior pii_mapping.

Request: { "text": "…", "pii_mapping": { "pseudonym": "original", ... } } Response: { "text": "…with real values restored…" }

curl -s -X POST https://your-firewall.example.com/v1/deanonymize \
  -H "Content-Type: application/json" \
  -d '{"text": "Email alex.morgan@example.com", "pii_mapping": {"alex.morgan@example.com": "john.smith@acme.com"}}'

Session-scoped mapping store — /v1/mappings

Bridges the stateless PreToolUse/PostToolUse hook lifecycle: one hook stores the mapping produced by inspection, the other retrieves it to de-anonymize tool output. In-memory, 1-hour TTL — not durable across a restart, and not a substitute for holding the mapping yourself when you can.

EndpointRequestResponse
POST /v1/mappings{ "session_id", "pii_mapping", "merge": true }merge (default true) merges into any existing mapping for the session; false replaces it.{ "session_id", "mapping_count" }
GET /v1/mappings/{session_id}{ "session_id", "pii_mapping" }, or 404 if no mapping exists for that session.
DELETE /v1/mappings/{session_id}{ "deleted": true|false }

Agent Security tool registry

API-key auth. Lets an integrating app declare its own tools' security profiles under its own key — separate from the dashboard's tenant-admin /v1/keys/{id}/config, which can edit any key. All three endpoints operate on the caller's authenticated key only; there is no way to target another key's registry from these routes. See Detect a compromised agent for how the registry feeds the action gate.

EndpointNotes
GET /v1/toolsReturns { "tools": {...}, "default": {...} | null } — the caller key's raw override, not the tenant-merged effective view.
POST /v1/toolsBody: { "tools": { "<name>": { "output_trust": "trusted"|"untrusted", "action_risk": "low"|"high" } }, "default": {...}, "replace": false }. Merges per-tool by default; replace: true swaps the whole set. default is applied only when the key is present in the body (even as null, to clear it). 400 if there is nothing to register (no non-empty tool profile and no default). Response echoes the registry plus count.
DELETE /v1/toolsClears the key's tool registry (tools + default), preserving its other config overrides. 204 on success.
curl -s -X POST https://your-firewall.example.com/v1/tools \
  -H "X-SFW-Key: sfw_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "tools": {
      "web_search": { "output_trust": "untrusted", "action_risk": "low" },
      "send_email": { "output_trust": "trusted",   "action_risk": "high" }
    },
    "default": { "output_trust": "untrusted", "action_risk": "low" }
  }'

Both 503 ("config store not configured" — the runtime-config store isn't wired on this deployment) and 403 ("per-key registration requires an authenticated API key" — the caller is anonymous) are possible; 403 is a client auth condition, not a server outage.

Transparent proxy

ANY /proxy/openai/{path} and ANY /proxy/anthropic/{path} — a transparent reverse proxy in front of the OpenAI and Anthropic APIs. Point your SDK's base_url at it; request and response bodies are otherwise the provider's native shape. See Transparent proxy for a full walkthrough.

Two-keys model

HeaderCarriesBehavior
X-SFW-KeyYour firewall keyAuthenticates you to the firewall; stripped before the request is forwarded upstream.
Authorization (OpenAI) / x-api-key (Anthropic)Your provider keyForwarded to the provider untouched; the firewall never stores it.

Optional headers

HeaderPurpose
X-SFW-Session-IdStable conversation id for multi-turn injection-pressure tracking and the agent action gate. ≤ 200 chars, no | or control characters; anything else is silently ignored (never an error).
X-Upstream-UrlOverride the upstream base URL — route through an OpenAI-compatible gateway (OpenRouter, Requesty, a Bedrock gateway, …).
X-ProviderSelect a named upstream from proxy.providers in server config, as an alternative to X-Upstream-Url.
X-SFW-Trace-Id / X-Correlation-Id / X-Request-IdSupply your own correlation id (sanitized, ≤ 200 chars); otherwise one is minted. Reflected back as X-SFW-Trace-Id.

Response headers

HeaderWhen
X-SFW-Trace-IdAlways.
X-SFW-Dry-Run: trueThe firewall is in monitor mode — nothing below was enforced.
X-SFW-Response-ActionThe output verdict (or action-gate verdict) was non-allow but not enforced as a block (e.g. flag).

Blocks

An input block or a non-streaming output/action-gate block comes back as a 403 with a provider-shaped error body plus a firewall extension object: { "issues": [...], "direction": "input"|"output", "confidence": ..., "trace_id": "..." }.

DirectionProvidererror.typeerror.code
InputOpenAIfirewall_blockcontent_policy_violation
InputAnthropicrequest_blocked
Output / action gateOpenAIfirewall_response_blockoutput_anomaly
Output / action gateAnthropicresponse_blocked
{
  "error": {
    "message": "Request blocked by semantic firewall",
    "type": "firewall_block",
    "code": "content_policy_violation"
  },
  "firewall": {
    "issues": [ { "type": "injection", "severity": "high", "detail": "..." } ],
    "direction": "input",
    "confidence": 1.0,
    "trace_id": "a1b2c3d4"
  }
}

Other proxy-specific error codes:

Statuserror.codeWhen
415multimodal_not_inspectedproxy.reject_multimodal is on and the request carries non-text content blocks (images, audio, …) the firewall can't inspect.
400firewall_streaming_not_permittedThe request streams (stream: true) while Agent Security's action gate is active and agent_security.reject_streaming is on — the gate can't inspect a streamed tool call, so the deployment fails closed instead of silently skipping the gate.

Streaming responses cannot be retroactively blocked (tokens are already delivered); an output or action-gate finding on a stream is recorded and surfaced via X-SFW-Response-Action only, never as a 403.

Utility / public

EndpointAuthResponse
GET /v1/healthnone{"status": "healthy"}
GET /v1/confignoneSnapshot of enabled engines, proxy status, and the action policy — {"engines": {...}, "proxy": {"enabled": bool}, "action_policy": {...}}.
GET /install.shnonePOSIX shell installer for the sfw CLI, text/x-shellscript. curl -fsSL https://your-firewall.example.com/install.sh | sh.
GET /cli/manifest.jsonnone{"server_version", "server_url", "install", "artifacts": [...]}.
GET /cli/{artifact}noneDownloads a CLI binary. artifact must match sfw-(darwin|linux|windows)-(amd64|arm64)[.exe]; 404 on any other name, 503 if this deployment doesn't serve binaries.

Dashboard-scoped (Clerk JWT) endpoints

These are called by the dashboard UI with a Clerk session token (Authorization: Bearer <clerk-jwt>), not documented here for direct machine use. Roles are owner / admin / member, scoped per tenant.

PathPurpose
POST /v1/keys, GET /v1/keys, GET /v1/keys/{id}, DELETE /v1/keys/{id}API key CRUD. Reading/deleting another user's key requires admin.
GET/PUT/DELETE /v1/keys/{id}/config, GET /v1/keys/{id}/config/history, GET /v1/keys/{id}/export, POST /v1/keys/{id}/playgroundPer-key runtime config: view, edit, clear, audit history, export, and a live test-inspect against the key's effective config.
GET/PUT /v1/tenant/config, GET /v1/tenant/config/historyTenant-wide runtime config (the fallback every key inherits from). PUT requires admin.
GET /v1/metrics/summary, /events, /timeseries, /events/{id}, /top-issuesDashboard activity, charts, and event detail.
POST /v1/webhooks/clerkClerk user/org lifecycle webhook, Svix-signature verified — not a dashboard-session call.

PII entities detected

The default entity set (engines.pii.entities):

PERSON, EMAIL_ADDRESS, PHONE_NUMBER, US_SSN, CREDIT_CARD, LOCATION, IP_ADDRESS, IBAN_CODE, URL, ORGANIZATION

Configurable per key or tenant via the config override's pii.entities.