Bulwark API reference

Everything needed to wire an agent into Bulwark: authentication, registration, the decision gate, the policy model, and how to verify the evidence chain without trusting us. Public and complete — no account required to read it.

Base URL https://api.bastionshieldtechnologies.com · Version v1 · Updated 3 September 2026

Bulwark gives every AI agent an identity, evaluates its actions against policy, routes risky ones to a human, and writes the whole thing to a tamper-evident audit trail.

Bulwark never blocks your agents. V1 runs in monitor mode: it observes, evaluates and records. A deny verdict is a recommendation and an audit record — acting on it happens in your code. Governance being down can never take your agent down.

#1. Availability and failure modes

The first question every platform engineer asks: what happens to my production agent when Bulwark is down or slow? The short answer is nothing. The long answer is worth reading before you put decide() on a request path.

#Bulwark is not in your data path

Bulwark is a control plane you call, not a proxy your traffic flows through. There is no network path where we can drop, delay or alter your agent's work. observe() is buffered and asynchronous. decide() is a synchronous call you choose to place where you want a verdict. If you never call us, nothing of yours changes.

#"Never blocks" and "fail-closed" are about different things

These two statements are both true, and the distinction matters:

  • Bulwark never blocks. We return a verdict. We have no mechanism to stop your agent

doing anything, including when the verdict is deny.

  • Fail-closed / fail-open describes which verdict you get when policy cannot be

evaluated — not whether anything is enforced. Acting on deny is always your code's decision.

An earlier version of these docs described a server-side fail-closed mode where high and critical risk tiers return deny if the policy engine is unavailable. That code path exists but is not reachable in the deployed service — nothing sets the flag that triggers it. Treat server-side fail-closed as unimplemented until this section says otherwise.

#What actually happens when the API is unreachable

CallSDK default (fail_open=True)fail_open=False
observe()Buffered, retried, never raisesNever raises — telemetry is not worth an outage
decide()Returns allow with degraded=TrueRaises BulwarkError for you to handle

The degraded flag is the important part. A fail-open verdict carries:

json
{
  "decision": "allow",
  "allowed": true,
  "degraded": true,
  "reason": "bulwark_unavailable: fail-open, no policy was evaluated"
}

Never treat a degraded result as a governance record. No policy was evaluated, and nothing was written to your evidence chain. If you need a hard stop when governance is unavailable, set fail_open=False and handle the exception — that decision belongs in your code, where the consequences are.

#Latency

decide() is deterministic rule matching with no model inference on the path.

MeasurementValue
POST /v1/decide p502 ms
POST /v1/decide p9915 ms
Throughput~13,500 req/s

Read the methodology before quoting these. They were measured with autocannon at 50 concurrent connections against a single Node process with an in-memory store, rate limiting disabled, on localhost. They measure application work, not a production round trip, and they exclude database latency and network time from your infrastructure to ours.

We have not published a production latency SLO, and we would rather say that than quote a number we cannot stand behind. If you need one for a procurement process, ask and we will benchmark against your region and volume.

#Availability

Single region (London), single instance, managed Postgres. There is no published uptime SLA on the free tier and no multi-region failover. This is a deliberate V1 constraint: the store is a single-instance snapshot, and a second instance would corrupt it. That is survivable precisely because we are not in your data path — but if you need a contractual availability guarantee, we are too early and you should say so now rather than after a security review.


#2. The one concept that matters: events vs decisions

This trips up almost everyone, so it comes first.

POST /v1/eventsPOST /v1/decide
What it isTelemetry. "This happened."The policy gate. "May this happen?"
Evaluates policyNo. Never.Yes
Returns202 {accepted}A verdict: allow / deny / escalate
StyleBuffered, batched, fire-and-forgetSynchronous, one call per action
Counts toward quotaNoYes
PopulatesActivity stream, agent timelines, Shadow-AIDecisions today, Flagged, Risk score, Approvals

If you only ever call /v1/events, your dashboard will show activity and zero decisions. That is correct behaviour, not a bug — nothing has been governed, only observed. The Overview tiles are driven entirely by events of type decision, which only /v1/decide produces.

Most integrations want both: observe() everywhere for the audit trail, decide() on the actions that actually carry risk (writes, spend, sends, deletes, anything touching customer data).


#3. Getting started, step by step

A complete first integration, from a fresh account to a governed decision you can see in the dashboard. Roughly 15 minutes. Every step is copy-pasteable and each one says how to tell it worked.

Steps A to G are the minimum. H to K are how you prove it is actually governing something.


#A. Create your account

Go to app.bastionshieldtechnologies.com/signup. You will get a 6-digit code by email to confirm the address.

You land on the free Developer plan: the full V1 platform, 100,000 governed decisions a month, no card. Nothing is charged, ever, on V1.

#B. Create an API key

Settings → API keys → Create key.

The secret is shown once. Copy it now; only a fingerprint is kept afterwards. If you lose it, revoke and make a new one.

bash
export BULWARK_KEY="bw_live_xxxxxxxxxxxx"     # paste yours
export BULWARK_URL="https://api.bastionshieldtechnologies.com"

An API key maps to the developer role. It can register agents, send events and request decisions. It deliberately cannot mint other keys, manage team members or change your plan, so a leaked key cannot escalate itself.

#C. Install the SDK (optional)

bash
pip install bulwark-sdk

Skip this if you would rather use REST. Every step below is shown both ways, and the REST path needs nothing installed.

#D. Register your first agent

An agent is anything that acts on your behalf: an LLM chain, a scheduled job, a deterministic rules engine. If it takes actions you would want a record of, register it.

bash
curl -sS $BULWARK_URL/v1/agents \
  -H "authorization: Bearer $BULWARK_KEY" \
  -H "content-type: application/json" \
  -d '{
    "name": "Portfolio Assistant",
    "purpose": "Answers investor questions about portfolio performance",
    "riskTier": "medium",
    "environment": "prod",
    "tools": ["portfolio.read", "ai.assistant.answer"],
    "dataSources": ["portfolios"]
  }'
python
from bulwark import BulwarkClient
bw = BulwarkClient(api_key=os.environ["BULWARK_KEY"])

agent = bw.register_agent(
    name="Portfolio Assistant",
    purpose="Answers investor questions about portfolio performance",
    risk_tier="medium",
    environment="prod",
    tools=["portfolio.read", "ai.assistant.answer"],
    data_sources=["portfolios"],
)
print(agent["agentId"])        # agt_xxxxxxxx  <- save this

Choosing the fields:

FieldHow to choose
nameWhat a human on your team calls it
purposeOne sentence. Shown to whoever reviews a flagged action, so write it for them
riskTierhigh/critical fail closed if Bulwark is unreachable. Use them for agents that spend, send or delete
environmentprod agents appear in Shadow-AI checks; dev ones do not
toolsThe actions this agent is allowed to take. This is enforced, see step I
dataSourcesWhat it reads. Descriptive
ownerUserIdOptional but recommended. Prod agents with no owner get flagged

Save the returned agentId. Put it in config or an environment variable. Do not re-register on every boot or you will create duplicate agents.

This is where most integrations go wrong. Not here, but at the next step, and later at step G. Read both carefully.

#E. Activate the agent

A newly registered agent is in state pending. /v1/decide denies every request from a non-active agent with agent_not_active. Activate it:

bash
curl -sS $BULWARK_URL/v1/agents/agt_XXXXXXXX/state \
  -H "authorization: Bearer $BULWARK_KEY" \
  -H "content-type: application/json" \
  -d '{"state": "active"}'
python
bw.activate_agent(agent["agentId"])

Check it worked: the response shows "state": "active". In the dashboard, Agent registry shows the agent as Active.

The full lifecycle: draft → pending → active → suspended → retired. retired is terminal.

#F. Send your first event

An event records that something happened. It is telemetry, appended to your audit chain. It does not evaluate policy.

bash
curl -sS $BULWARK_URL/v1/events \
  -H "authorization: Bearer $BULWARK_KEY" \
  -H "content-type: application/json" \
  -d '{
    "batchId": "'"$(uuidgen)"'",
    "events": [{
      "agentId": "agt_XXXXXXXX",
      "type": "action",
      "payload": { "name": "portfolio.read", "resource": "portfolios", "onBehalfOf": "user_88" }
    }]
  }'
python
bw.observe(agent["agentId"], "portfolio.read", resource="portfolios", on_behalf_of="user_88")
bw.flush()      # the SDK batches; flush forces the send

Check it worked: {"accepted": 1, "rejected": 0, "deadLettered": 0}. In the dashboard, Overview → Recent activity shows it within seconds.

If deadLettered is 1, the agentId does not exist. Bulwark captures rather than rejects it, so a typo fails quietly. Look at GET /v1/events/dead-letter.

Never send raw prompts, completions or customer documents. Content fields are stripped at ingest by design, but the cheapest data to protect is the data you never send.

#G. Make your first governed decision

This is the step that separates observation from governance.

bash
curl -sS $BULWARK_URL/v1/decide \
  -H "authorization: Bearer $BULWARK_KEY" \
  -H "content-type: application/json" \
  -d '{
    "agentId": "agt_XXXXXXXX",
    "actionType": "portfolio.read",
    "resource": "portfolios"
  }'
python
verdict = bw.decide(agent["agentId"], "portfolio.read", resource="portfolios")
print(verdict.decision, verdict.reason)
json
{ "decisionId": "dec_…", "decision": "allow", "allowed": true, "pending": false,
  "reason": "no matching policy — default allow", "approvalId": null }

Check it worked: Overview → Decisions today goes from 0 to 1.

Bulwark does not block. You get a verdict; acting on it is your code's job:

python
verdict = bw.decide(agent_id, "crm.write", resource="customers")
if verdict.degraded:
    pass                                      # Bulwark unreachable. NOT a governance record
elif verdict.pending:
    log.warning("awaiting human approval: %s", verdict.approval_id)
elif not verdict.allowed:
    log.warning("flagged: %s", verdict.reason)
proceed()                                     # monitor mode: you decide what to do
If "Decisions today" stays at 0 while events arrive, you are only calling /v1/events. That is the single most common integration mistake. Events are telemetry; decisions come from /v1/decide.

#H. See it in the dashboard

WhereWhat you should see
OverviewDecisions today ≥ 1, active agent count, recent activity
Agent registry → your agentIts timeline: registered, state change, action, decision
EvidenceEvery decision as a hash-chained receipt
ApprovalsEmpty for now. Fills once something escalates (step J)

#I. Constrain the agent with its declared tools

Remember tools from step D. It is an allowlist, and it is enforced: an action that matches no declared tool is escalated as action_outside_scope.

Our example agent declared portfolio.read and ai.assistant.answer. So:

bash
curl -sS $BULWARK_URL/v1/decide \
  -H "authorization: Bearer $BULWARK_KEY" \
  -H "content-type: application/json" \
  -d '{"agentId": "agt_XXXXXXXX", "actionType": "crm.write", "resource": "customers"}'
json
{ "decision": "escalate", "allowed": false, "pending": true,
  "reason": "action_outside_scope: crm.write is not in the agent's declared tools",
  "obligations": ["review_scope"], "approvalId": "apr_…" }

That is out-of-scope detection with no policy written at all. Declare tools accurately and you get it for free. An empty tools list disables the check for that agent.

Action names glob, so crm.* in tools permits crm.read and crm.write.

#J. Add a policy (optional)

Policies express rules the tool allowlist cannot. Per-agent scoping works directly on agentId:

bash
curl -sS $BULWARK_URL/v1/policies \
  -H "authorization: Bearer $BULWARK_KEY" \
  -H "content-type: application/json" \
  -d '{
    "name": "High-risk writes need a human",
    "enforcement": "monitor",
    "rules": [{
      "match": { "action": "*.write" },
      "when": { "riskTier": { "in": ["high", "critical"] } },
      "decide": "escalate",
      "approvers": ["role:approver"],
      "ttlMinutes": 60
    }]
  }'

Escalations appear in Approvals, where a human accepts or rejects with a reason. Both the request and the verdict are hash-chained, which is what makes it evidence of human oversight rather than a claim of it. See §8 for the full rule model.

#K. Verify your evidence independently

The point of the audit trail is that you do not have to trust us.

bash
curl -sS "$BULWARK_URL/v1/evidence/bundle" \
  -H "authorization: Bearer $BULWARK_KEY" -o bundle.json

node tools/verify-evidence.mjs bundle.json

The verifier has zero dependencies and re-implements the hash rule rather than importing Bulwark code, so it does not execute anything of ours. Linkage verifies with no secret; supply the HMAC key for full cryptographic verification.


#Checklist

  • [ ] Account created, on the free Developer plan
  • [ ] API key created and stored as BULWARK_KEY
  • [ ] Agent registered, agentId saved to config
  • [ ] Agent activated (state active, not pending)
  • [ ] First event accepted, visible in Recent activity
  • [ ] First decision made, Decisions today ≥ 1
  • [ ] Out-of-scope action returns escalate (proves scope enforcement)
  • [ ] Evidence bundle exported and verified offline

#Where to go next

  • Instrument every path, not just the obvious one. The most common failure is not a broken

pipeline, it is code paths that never emit. For each agent, point at the line that reports what it did. If you cannot, you are assuming, not observing.

  • §4 for your architecture (serverless must flush before the handler returns).
  • §12 when something looks wrong.

#4. Authentication

Two credential types, both Authorization: Bearer <token>.

API keySession token
Looks likebw_live_…, bw_dev_…JWT
ForServer-to-server, your agentsThe dashboard
Obtained viaSettings → API keysOAuth2 code + PKCE
Maps to roledeveloperThe user's real role

An API key cannot: mint or revoke other keys, manage team members, change the plan, or rename the organisation. Those need a human session — a leaked key cannot escalate itself.

Keys are stored as a SHA-256 hash; the cleartext is never persisted. Revoke instantly with POST /v1/api-keys/:keyId/revoke.

#Error responses

StatusBody errorMeaning
400invalid_requestSchema validation failed. details has field-level errors.
401unauthorizedMissing, malformed, expired or revoked credential.
402upgrade_requiredYour plan lacks this feature. feature names it.
403admin_required / use_break_glassAuthenticated but not permitted.
409email_already_registeredSignup conflict.
413payload_too_largeBody over 256 KB.
429rate_limited / account_lockedHonour retry-after.
500internalOur fault. Safe to retry with the same batchId.

#5. Integration patterns by architecture

Bulwark is a plain JSON API over HTTPS, so it fits any stack. What changes is where you put the calls and when you flush.

#4.1 Monolith / long-running service

The easy case. One client for the process lifetime; the background flusher batches events.

python
from bulwark import BulwarkClient
bw = BulwarkClient(api_key=os.environ["BULWARK_KEY"])   # module-level, reused

#4.2 Microservices

One client per service. Register each agent once (at deploy or first boot), then store the agentId in config — do not re-register on every start, or you will create duplicate agents.

#4.3 Serverless (Lambda, Cloud Functions, Vercel)

You must flush before the handler returns. A frozen or torn-down container never runs the background flusher, and buffered events are lost.

python
def handler(event, context):
    bw.observe(AGENT_ID, "invoice.process", resource="invoices")
    ...
    bw.flush(timeout=2.0)      # ← non-negotiable
    return {"statusCode": 200}

Prefer a module-level client so it survives warm invocations.

#4.4 Queue / worker pools

Every worker gets its own client. The per-IP rate limit (120 burst, 20/s) is shared across a NAT'd fleet — the SDK's jittered backoff exists for exactly this. If you run a large fleet, raise flush_interval so workers do not synchronise.

#4.5 Framework-based agents (LangChain, LlamaIndex, CrewAI)

No official callback handler yet. Instrument at your tool boundary — wherever the framework actually invokes a tool is where the governance call belongs:

python
def governed_tool(name, fn):
    def wrapper(*args, **kwargs):
        verdict = bw.decide(AGENT_ID, name, resource=kwargs.get("resource"))
        if not verdict.allowed:
            log.warning("Bulwark flagged %s: %s", name, verdict.reason)
        bw.observe(AGENT_ID, name, context={"verdict": verdict.decision})
        return fn(*args, **kwargs)          # monitor mode: we still run it
    return wrapper

#4.6 Not available yet

Sidecar container, MCP interceptor, and enforcement/blocking are roadmap, not shipped. Anything suggesting otherwise is aspirational.


#6. SDKs and install options

The REST API in §2 needs no install and always works. If you want to be running in ten minutes, use it. Everything below is convenience on top of the same three calls.

LanguageStatus
REST / cURLSupported, zero install, any language
PythonAvailable. Install options below
TypeScriptIn-repo, unpublished
Go, Java, RustNot built. Use REST

#Installing the Python SDK

Pick whichever works for you today:

bash
pip install bulwark-sdk

Published on PyPI, Python 3.9+, zero runtime dependencies.

If your organisation vendors dependencies rather than installing them, copy src/bulwark/ into your project instead. It is four files and about 640 lines with no imports beyond the standard library, so it drops in unchanged.

python
from bulwark import BulwarkClient

bw = BulwarkClient(api_key="bw_live_...")          # fail_open=True by default
agent = bw.register_agent(name="...", purpose="...", risk_tier="medium", environment="prod")
bw.activate_agent(agent["agentId"])

bw.observe(agent["agentId"], "crm.read", resource="customers")

verdict = bw.decide(agent["agentId"], "crm.write", resource="customers")
if verdict.degraded:
    ...        # Bulwark was unreachable; this is NOT a governance record
elif not verdict.allowed:
    log.warning("flagged: %s", verdict.reason)

bw.flush()

The SDK is zero-dependency (stdlib only), so adopting Bulwark adds no supply-chain surface to your agent runtime. It batches up to 100 events per request, retries 429/5xx with jittered backoff, and observe() never raises into your hot path.

#Rolling your own

Entirely reasonable, and our first integrator did exactly this in about 200 lines of urllib. You need three calls: register an agent, POST events, POST decide. Do these four things and you will match the SDK:

  • Send a batchId per event batch so retries are idempotent.
  • Cap batches at 100 events.
  • Retry only 429 and 5xx, honouring retry-after, with jitter.
  • Never let a Bulwark failure raise into your agent. Swallow and log.

#7. REST reference

#Agents

POST /v1/agents — register.

FieldTypeRequiredNotes
namestring
purposestringWhat it is for; shown to reviewers
riskTierlow\medium\high\criticalHigh/critical fail closed on outage
environmentdev\staging\prod
toolsstring[]Declared capability allowlist — see §8.3
dataSourcesstring[]
modelsUsedstring[]
scopestringe.g. "UK customer accounts only"
categoryenumSales, Finance, HR, Engineering, Operations, Security, Data, Marketing, Support
spendLimitGbpnumber
ownerUserIdstringUnowned prod agents are flagged by Shadow-AI

GET /v1/agents · GET /v1/agents/:agentId · GET /v1/agents/:agentId/authority

POST /v1/agents/:agentId/state{"state": "active"}. Legal transitions:

text
draft → pending → active → suspended → active
                     ↓         ↓
                  retired ← retired          (retired is terminal)

#Events

POST /v1/events202

FieldTypeNotes
batchIdstringSend one. Makes retries idempotent — a replayed batch returns the original result with idempotentReplay: true instead of double-appending to the chain.
events[]array1–100 per request
events[].agentIdstringMust be registered, else dead-lettered
events[].typeaction\decision\approval\config
events[].payloadobjectname is the action; resource, onBehalfOf conventional

Response: {accepted, rejected, deadLettered, hashes[]}

GET /v1/events · GET /v1/events/dead-letter · POST /v1/events/replay

#Decide

POST /v1/decide200

FieldTypeRequired
agentIdstring
actionTypestring✅ e.g. crm.write, financial.transfer
resourcestring
dataClassificationstring
contextobjectAnything here is available to policy when conditions

Response: {decisionId, decision, allowed, pending, policyId, reason, obligations[], approvalId}

#Policies

GET|POST /v1/policies · GET|PUT /v1/policies/:policyId · GET /v1/policy-bundle

json
{ "name": "Protect customer writes",
  "enforcement": "monitor",
  "rules": [{
    "match": { "action": "crm.*" },
    "when": { "riskTier": { "in": ["high", "critical"] } },
    "decide": "escalate",
    "approvers": ["role:approver"],
    "ttlMinutes": 60,
    "obligations": ["notify_owner"]
  }]}

#Evidence · Approvals · Shadow-AI · Keys · Team

POST /v1/verify-chain · GET /v1/evidence/bundle · GET /v1/evidence/siem?format=splunk|sentinel|jsonl GET /v1/receipts · GET /v1/receipts/:seq GET /v1/approvals · POST /v1/approvals/:approvalId · GET /v1/approvals/evidence GET /v1/shadow · POST /v1/shadow/scan · POST /v1/shadow/:id/register|quarantine|dismiss GET|POST /v1/api-keys · POST /v1/api-keys/:keyId/revoke GET /v1/team · POST /v1/team/invite GET /v1/overview · GET /v1/billing · GET /v1/minimization · GET /v1/monitor


#8. Policies and scope

#7.1 Evaluation

Rules are checked in order. A rule's when clause can match on any of these facts:

FactSource
agentIdThe agent being evaluated
actionTypeThe action being requested
resourceFrom the request
dataClassificationFrom the request
agentStateRegistry: draft/pending/active/suspended/retired
riskTierRegistry: low/medium/high/critical
environmentRegistry: dev/staging/prod
anything in contextWhatever you passed. Overrides a built-in fact of the same name

Per-agent policies are the common case, so agentId and actionType are matchable directly:

json
{ "match": { "action": "*" },
  "when": { "agentId": { "eq": "agt_portfolio" },
            "actionType": { "nin": ["portfolio.read", "ai.assistant.answer"] } },
  "decide": "escalate" }

Operators: eq ne gt gte lt lte in nin exists. Unknown operators fail safe (the condition cannot be satisfied).

Action patterns glob: crm.* matches crm.read and crm.write; * matches everything.

Precedence: most restrictive wins — deny > escalate > allow. No matching rule → allow.

#7.2 Built-in, with no policy authored

  • agent_not_active — a pending, suspended or retired agent is denied.
  • Fail-closed on outage for high / critical risk tiers.

#7.3 Scope conformance — tools is enforced

Declaring tools: ["crm.read"] is a claim about what the agent may do, and Bulwark checks it.

If an action matches no declared tool, and no policy explicitly decided the action, the verdict is escalate with reason: "action_outside_scope: …" and obligations: ["review_scope"]. A human approval is raised.

bash
# agent registered with tools: ["crm.read"]
curl -sS .../v1/decide -d '{"agentId":"agt_X","actionType":"crm.write"}'
# -> {"decision":"escalate","reason":"action_outside_scope: crm.write is not in
#     the agent's declared tools","obligations":["review_scope"],"approvalId":"apr_…"}

Rules that matter:

  • An explicit policy wins. Scope only speaks where the engine would otherwise default-allow, so

a deliberate policy authorising an action is never overridden by the declared list.

  • No tools means the gate is off. An agent that declared nothing is never penalised — this is

opt-in by declaration, not a breaking default.

  • ai.* is exempt. Model inference is intrinsic to being an agent, not a tool grant.
  • Globs work. tools: ["crm.*"] covers crm.read and crm.write.
  • It escalates, it does not block. Monitor mode is unchanged.

On the events path too. Monitor rule P9 (Out-of-scope action) applies the same allowlist and the same glob semantics to the event stream, so a tenant that only calls /v1/events and never /v1/decide still sees out-of-scope activity in Detected anomalies. Run POST /v1/monitor/scan or wait for the scheduled scan.

#9. Evidence and independent verification

Every governance event appends to a per-tenant hash chain:

text
hash = HMAC-SHA256(key, `${schemaVersion}:${prevHash}:${canonicalJson(payload)}`)

Because each entry commits to its predecessor, altering or deleting any historical record breaks every hash after it.

Export with GET /v1/evidence/bundle, then verify offline, without running our code:

bash
node tools/verify-evidence.mjs bundle.json

The verifier is zero-dependency and re-implements the hash rule independently — it does not import Bulwark. Linkage verifies with no secret at all; supply the HMAC key for full cryptographic verification. This is the point: you do not have to trust us to trust the audit trail.

SIEM export: Splunk HEC, Microsoft Sentinel, or JSONL via GET /v1/evidence/siem?format=….


#10. Limits and reliability

LimitValue
Events per batch100
Request body256 KB
Rate limit120 burst, 20/s, per IP
Payload truncationIndividual payloads > 32 KB are replaced with a marker
Decision quota (free)100,000/month — soft; never blocks

Retry only 429 and 5xx, honouring retry-after, with jittered exponential backoff. Retrying a 4xx just burns your rate-limit bucket.

Dead-letter, not reject: an event for an unregistered agentId is captured at GET /v1/events/dead-letter rather than dropped — so a typo is recoverable, but silent. Check it if events seem to vanish.

Exceeding quota raises a notice and an upgrade prompt. It does not block ingestion, does not block decisions, and costs nothing on the free tier.


#11. Data minimization

Enforced at ingest, before anything is stored or hashed.

Stripped wholesale (19 keys): prompt prompts completion completions messages message content text body input output response request document attachment raw email_body transcript

Redacted by pattern: card numbers, emails, bearer/API tokens, US SSN, IBAN, phone numbers.

Every minimized payload carries a _min marker recording which keys were stripped and which redactions fired, so the transformation is auditable rather than invisible.

Do not send raw prompts, completions or customer documents. The filter is a safety net, not your control. The cheapest data to protect is the data you never transmit.


#12. Plans

FeatureDeveloper (free)TeamScaleEnterprise
Shadow-AI discovery
Approvals / human oversight
Evidence + SIEM export
Compliance packs
Integrations
Decisions / month100k250k2MVolume

The free Developer plan is the full V1 platform. Paid tiers are not self-serve yet.


#13. Troubleshooting

"Decisions today: 0" but events are arriving. You are only calling /v1/events. Telemetry never evaluates policy — call /v1/decide for the actions you want governed. See §1.

My events vanished. Check GET /v1/events/dead-letter. An unregistered agentId is dead-lettered, not rejected.

/v1/decide always denies with agent_not_active. The agent is still pending. Call POST /v1/agents/:id/state with {"state":"active"}.

I declared tools but out-of-scope actions aren't flagged. Check: is the action ai.* (exempt)? Is tools actually non-empty on the agent? Did an explicit policy already allow it (policy wins over scope)? On the events path, P9 findings appear after a scan — POST /v1/monitor/scan. See §8.3.

Shadow-AI finds nothing. Its detectors need signals: unregistered agent IDs in the stream, missing owners, stale keys, or existing policy denials. A tenant that only sends events for registered agents correctly produces no findings.

Everything returns 401. Check the Bearer prefix, and that the key is not revoked. Session tokens expire; API keys do not.

429s under load. The limit is per IP, so a NAT'd fleet shares one bucket. Honour retry-after and add jitter.


#14. Not available yet

Enforcement / blocking · MCP interception · sidecar container · SSO / SAML / SCIM · self-host and VPC deployment · published SDK packages beyond Python.

Support: info@bastionshieldtechnologies.com