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 key — sfw_live_xxxxxxxx… or sfw_test_xxxxxxxx…. Send it as:
X-SFW-Key: sfw_live_xxxxxxxx…— preferred, especially on the proxy whereAuthorizationcarries your upstream provider's key.Authorization: Bearer sfw_live_xxxxxxxx…— accepted anywhere except the proxy, whereAuthorizationis 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 inboundX-Request-Idor mints one) andx-processing-time-ms. -
Request bodies over 256 KB (
Content-Lengthbased) are rejected before parsing with413:{ "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):
| Field | Type | Required | Notes |
|---|---|---|---|
input_text | string | yes | Minimum 1 character. |
system_goal | string | no | The LLM's intended goal/system prompt, used as the semantic-drift baseline. |
request_id | string | no | Caller-supplied correlation id. |
session_id | string | no | Correlates turns of one conversation for cross-turn injection-pressure tracking. None = stateless, the default. |
metadata | dict[string,string] | no | Freeform, 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):
| Field | Type | Notes |
|---|---|---|
action | "allow" | "flag" | "block" | The recommended decision. |
confidence | number, 0–1 | Agreement across detection channels, not severity — a clean verdict is 1.0. |
issues | array of Issue | Empty when clean. |
sanitized_input | string | null | PII-pseudonymized text; null if no PII was found. |
pii_mapping | dict[string,string] | null | {pseudonym: original}. Keep this client-side to restore real values later. |
processing_time_ms | number | Server-side processing time. |
dry_run | boolean | true when the firewall is in monitor mode — action is still the real would-be decision, but nothing was enforced. |
Each Issue:
| Field | Type | Notes |
|---|---|---|
type | "pii" | "injection" | "drift" | |
severity | "low" | "medium" | "high" | "critical" | May be escalated one rank when ≥2 channels corroborate. |
detail | string | Human-readable reason. |
span | {start, end} | omitted | Character offsets, raw view only. |
view | string | omitted | The normalized view the issue was found in, e.g. base64-decoded, unicode-fold. Omitted for the raw input. |
channel | string | omitted | The 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:
| Field | Type | Required |
|---|---|---|
response_text | string | yes |
system_goal | string | no |
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:
| Field | Type | Required |
|---|---|---|
input_text | string | yes |
system_goal | string | no |
request_id | string | no — used as the task id if provided |
metadata | dict[string,string] | no |
webhook_url | string | yes |
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.
| Endpoint | Request | Response |
|---|---|---|
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.
| Endpoint | Notes |
|---|---|
GET /v1/tools | Returns { "tools": {...}, "default": {...} | null } — the caller key's raw override, not the tenant-merged effective view. |
POST /v1/tools | Body: { "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/tools | Clears 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
| Header | Carries | Behavior |
|---|---|---|
X-SFW-Key | Your firewall key | Authenticates you to the firewall; stripped before the request is forwarded upstream. |
Authorization (OpenAI) / x-api-key (Anthropic) | Your provider key | Forwarded to the provider untouched; the firewall never stores it. |
Optional headers
| Header | Purpose |
|---|---|
X-SFW-Session-Id | Stable 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-Url | Override the upstream base URL — route through an OpenAI-compatible gateway (OpenRouter, Requesty, a Bedrock gateway, …). |
X-Provider | Select 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-Id | Supply your own correlation id (sanitized, ≤ 200 chars); otherwise one is minted. Reflected back as X-SFW-Trace-Id. |
Response headers
| Header | When |
|---|---|
X-SFW-Trace-Id | Always. |
X-SFW-Dry-Run: true | The firewall is in monitor mode — nothing below was enforced. |
X-SFW-Response-Action | The 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": "..." }.
| Direction | Provider | error.type | error.code |
|---|---|---|---|
| Input | OpenAI | firewall_block | content_policy_violation |
| Input | Anthropic | request_blocked | — |
| Output / action gate | OpenAI | firewall_response_block | output_anomaly |
| Output / action gate | Anthropic | response_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:
| Status | error.code | When |
|---|---|---|
415 | multimodal_not_inspected | proxy.reject_multimodal is on and the request carries non-text content blocks (images, audio, …) the firewall can't inspect. |
400 | firewall_streaming_not_permitted | The 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
| Endpoint | Auth | Response |
|---|---|---|
GET /v1/health | none | {"status": "healthy"} |
GET /v1/config | none | Snapshot of enabled engines, proxy status, and the action policy — {"engines": {...}, "proxy": {"enabled": bool}, "action_policy": {...}}. |
GET /install.sh | none | POSIX shell installer for the sfw CLI, text/x-shellscript. curl -fsSL https://your-firewall.example.com/install.sh | sh. |
GET /cli/manifest.json | none | {"server_version", "server_url", "install", "artifacts": [...]}. |
GET /cli/{artifact} | none | Downloads 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.
| Path | Purpose |
|---|---|
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}/playground | Per-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/history | Tenant-wide runtime config (the fallback every key inherits from). PUT requires admin. |
GET /v1/metrics/summary, /events, /timeseries, /events/{id}, /top-issues | Dashboard activity, charts, and event detail. |
POST /v1/webhooks/clerk | Clerk 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.