AI Security & Observability — Direct API
For most external integrations, calls to /chat/completions are enough — moderation against the team's policy and tracing into Observability happen automatically inside the Gateway pipeline. See AI Gateway Integration.
This page covers the advanced path: external agents that own a multi-step trace, applications that moderate non-LLM content, and platforms that need to register agents or push their own spans into Observability.
When the default path is enough
The Gateway already handles both Security and Observability inline when:
- The team has
Enable Securityset and a policy assigned in the Gateway UI under Teams. - The team has
Enable Observabilityset in the same view.
If your application only calls LLMs and both toggles are on, you do not need anything on this page. Send x-beak-agent-id (and optionally x-beak-user-id, x-beak-session-id) on each /chat/completions request and the traces will appear under that agent in Observability.
Reach for the direct APIs below only for the patterns described next.
Pattern 1 — Owning the trace
Use this when your agent runs a multi-step loop (planner → tools → reflection → LLM call → ...) and you want the full sequence in a single trace, with LLM calls as nested spans rather than separate one-span traces.
Mint a trace id and parent span
At the start of the agent run, generate a trace_id. Treat the agent run itself as the root span and assign it a span_id.
Stitch LLM calls
On every Gateway /chat/completions request, send:
x-beak-trace-id: <your trace_id>
x-beak-span-id: <id you choose for this LLM span>
x-beak-parent-span-id: <id of the step that triggered the LLM call>
When the Gateway sees x-beak-trace-id, it skips the trace-row insert (your application owns the trace) and emits only the span with the ids you supplied. The LLM span will appear as a child of parent_span_id in the trace tree.
Emit the trace row yourself
Once your application owns the trace, it must emit the trace row. Post to:
POST {base_url}/aiobservability/ingest/traces
Body:
{
"traces": [
{
"trace_id": "trace-abc123",
"agent_id": "support-agent-v2",
"agent_name": "Support Triage",
"user_id": "user-42",
"session_id": "session-xyz",
"start_time": "2026-05-06T14:00:00Z",
"end_time": "2026-05-06T14:00:05Z",
"status": "ok",
"total_tokens": 1240,
"prompt_tokens": 800,
"completion_tokens": 440,
"total_cost": 0.0042,
"model": "gpt-4o-mini",
"metadata": {"team_id": "support", "version": "v2.3.1"}
}
]
}
Required headers:
Authorization: Bearer <jwt> # or API-Key: <token>
tenantid: <tenant-id>
Content-Type: application/json
Emit non-LLM spans
For tool calls, retrievals, and any other steps you want visible in the trace tree, post spans to:
POST {base_url}/aiobservability/ingest/spans
Body:
{
"spans": [
{
"span_id": "span-tool-1",
"trace_id": "trace-abc123",
"parent_span_id": "span-root",
"name": "tool.search_kb",
"service_name": "support-agent",
"kind": "internal",
"start_time": "2026-05-06T14:00:01Z",
"end_time": "2026-05-06T14:00:02Z",
"status": "ok",
"input_value": "{\"query\": \"refund policy\"}",
"output_value": "[3 documents]",
"attributes": {"tool": "search_kb", "result_count": 3},
"events": []
}
]
}
Span fields
| Field | Required | Notes |
|---|---|---|
span_id | Yes | Globally unique within your trace. |
trace_id | Yes | Matches the trace you emitted (or supplied via x-beak-trace-id). |
parent_span_id | When nested | Empty string for root span. |
name | Yes | Convention: llm.<model> for LLM, tool.<name> for tools, retrieval.<source> for RAG. |
service_name | Yes | Your service / agent identifier. |
kind | Yes | One of internal, server, client, producer, consumer. |
start_time / end_time | Yes | ISO-8601 with timezone. |
status | Yes | One of ok, error, unset. |
gen_ai_* | LLM only | Token counts and cost. Leave default 0 for non-LLM spans. |
input_value / output_value | No | Stringified inputs and outputs. JSON-encode complex values. |
attributes | No | Free-form key-value metadata. |
events | No | Reserved for span events (errors, retries). |
Batching
Both endpoints accept batches. Limits: traces up to 1000 per request, spans up to 5000 per request. Batching is fire-and-forget from the Gateway's own callback today; mirror that pattern in your code — observability calls must never block the user-facing path.
Pattern 2 — Moderating non-LLM content
Use this when content needs policy scanning even when no LLM call is happening: tool arguments, RAG retrievals, file uploads, or content from upstream systems.
Endpoint
POST {base_url}/aisecurity/inline/scan
Headers
Authorization: Bearer <jwt> # or API-Key: <token>
Content-Type: application/json
tenantid, userid, and roleid are injected by the Gateway from the JWT. Do not set them yourself.
Request
{
"messages": [
{"role": "user", "content": "<text to scan>"}
],
"agent_id": "support-agent-v2",
"security_policy": "Healthcare Compliance",
"project_id": null,
"guardrail_ids": null,
"rag_context": ["snippet 1", "snippet 2"],
"response": "<optional LLM response to also scan>"
}
Validator selection
Exactly one of these resolves which validators run:
guardrail_ids— A list of guardrail ids. Only those guardrails' validators are evaluated.security_policy— A policy name. The policy's guardrails resolve to a validator set.project_id— A project id. Validators come from the project's guardrails.- None — All validators run (heaviest path; reserve for ad-hoc scans).
Note: Pass
agent_idto attribute the scan to a registered agent in Pattern 3 — Registering an Agent. The action is logged against the agent profile and surfaces in its action history.
Response
{
"status": "success",
"message": "Moderation complete",
"data": {
"compliant": false,
"action": "block",
"legacy_action": "deny",
"risk_score": 80.0,
"results": [
{"validator": "pii_email", "compliant": false, "reason": "Email detected", "details": {}}
],
"findings": [...],
"redacted_text": "<text with matches redacted>",
"policy_id": 7,
"fired_rules": ["block_pii"],
"reason": "PII detected in input",
"event_id": "evt-...",
"timestamp": "2026-05-06T14:00:00Z"
}
}
Verdicts
action— Five-value vocabulary from the rule engine:allow,log,warn,redact,block.legacy_action— Three-value mapping for callers that gate on a single boolean:allow,review,deny. Maplog→allow,warnandredact→review,block→deny.risk_score—0.0–80.0. Synthesized from the action so existing dashboards keep working.redacted_text— Only populated whenactionisredact. Use this in place of the original text downstream.event_id— Audit-log identifier. Surface it in your error UI so the security team can pivot to the Forensic Explorer entry.
Applying the verdict in your code
import httpx
resp = httpx.post(
f"{BASE_URL}/aisecurity/inline/scan",
headers={"Authorization": f"Bearer {JWT}"},
json={
"messages": [{"role": "user", "content": user_text}],
"agent_id": "support-agent-v2",
"security_policy": "Healthcare Compliance",
},
timeout=10,
)
data = resp.json()["data"]
if data["legacy_action"] == "deny":
raise PolicyBlocked(reason=data["reason"], event_id=data["event_id"])
if data["action"] == "redact":
user_text = data["redacted_text"]
if data["legacy_action"] == "review":
flag_for_human_review(event_id=data["event_id"])
# allow → continue
Agent attestation (optional)
When an agent has been issued a cryptographic key (see Pattern 3), sign each moderation request and include:
x-agent-signature: <ed25519 signature of the raw body>
x-agent-timestamp: <signed timestamp>
x-agent-nonce: <random nonce>
The Security service verifies the signature using the agent's stored public key. A failed verification returns 403 Agent attestation failed. Skip these headers if the agent is not configured to require attestation.
Pattern 3 — Registering an Agent
Register every external agent in the Agent Registry so its actions appear in audit logs and its allowed scopes / risk threshold are enforced.
Create
POST {base_url}/aisecurity/agents
{
"agent_id": "support-agent-v2",
"name": "Support Triage Agent",
"description": "Triages incoming support tickets",
"permissions": {},
"risk_threshold": 50,
"allowed_scopes": ["read"],
"status": "active",
"generate_keypair": false,
"require_attestation": false
}
risk_threshold is 0–100. Lower is stricter. Set generate_keypair: true to create an Ed25519 keypair on registration — the private key is returned once in the response.
Generate, rotate, or revoke a key
POST {base_url}/aisecurity/agents/{id}/generate-key
POST {base_url}/aisecurity/agents/{id}/rotate-key { "reason": "<text>" }
POST {base_url}/aisecurity/agents/{id}/revoke-key { "reason": "<text>" }
The private key value is returned exactly once on generate-key and rotate-key. The server stores only the public key; it cannot recover a lost private key — generate a new one and rotate.
List the agent's actions
GET {base_url}/aisecurity/agents/{id}/actions
Returns every audited action attributed to the agent — moderation requests, blocks, attestation failures — for compliance reporting.
Pattern 4 — Querying the Audit Log
The audit log is hash-chained — every entry includes a content_hash and previous_hash so tampering is detectable. Query it via:
GET {base_url}/aisecurity/audits
Use this for compliance evidence and forensic review. The same data underpins the Audit Log and Forensic Explorer UIs.
Best Practices
- Send observability calls fire-and-forget. The user-facing path must never block on a trace ingest. Mirror what the Gateway does: post in a background task and swallow errors at debug log level.
- Reuse one
httpx.AsyncClient(or equivalent) across calls. Both ingest endpoints support batching — buffer spans and traces in memory and flush every few seconds rather than posting one-at-a-time. - Keep span
namevalues stable. Observability rolls up by name when ranking top spans; renamingtool.search_kbtotool.searchKBhalfway through a release fragments the dashboards. - For non-LLM moderation, prefer
security_policyover passingguardrail_idsdirectly. The policy is a stable contract; guardrail ids may change as the security team reorganizes the validator hierarchy. - Do not call
/aisecurity/inline/scanfrom inside an agent loop that already routes through the Gateway. The Gateway already moderated the LLM messages — calling again duplicates audit entries and double-charges the rule engine. - Treat the
event_idreturned by every moderation call as a first-class field. Log it, surface it in error UIs, and forward it in support tickets so the security team can pivot directly to the audit entry. - Register every agent in the Agent Registry, even low-risk internal ones. The registry is the join key between traces, moderation events, and forensic sessions — agents that aren't registered show up as anonymous in compliance views.