REST API

Send events directly to SignalVault. No SDK required — use any HTTP client in any language.

Base URL

https://api.signalvault.io

Authentication

All API requests require a Bearer token:

Authorization: Bearer sk_live_your_api_key

POST /v1/events

Ingest an event into SignalVault.

Event types

There are two kinds of events: ones you submit, and ones SignalVault generates internally.

TypeSubmitted byDescription
ai.requestYouSend before calling your AI provider. SignalVault evaluates rules and returns a decision.
ai.responseYouSend after your AI provider responds. SignalVault runs post-flight rules and records cost.
ai.errorYouSend if your AI provider call fails (timeout, rate limit, etc.). Logged for observability.
agent.tool_callYouAudit a tool call made by an AI agent. Rules are evaluated against the tool's input and output, so this can return a block decision too.
policy.violationSignalVaultAutomatically created when a guardrail rule fires. Cannot be submitted directly.
budget.exceededSignalVaultAutomatically created when a cost or token budget is exceeded. Cannot be submitted directly.

Submitting a policy.violation or budget.exceeded event directly will return a 422 error. These are generated by SignalVault automatically — you never need to send them yourself.

Request body fields

FieldTypeRequiredDescription
typestringYesSee event types above
request_idstringUsuallyCorrelates the events of one call. Generated for you on an ai.request if omitted; required on ai.response and ai.error, and optional on agent.tool_call.
event_idstringNoIdempotency key — safe to retry with the same value
environmentstringIgnoredTaken from your API key. A value sent here is overwritten, so guardrails scoped to one environment cannot be skipped.
providerstringNoAny provider name you use, for grouping in the dashboard. Defaults to openai.
modelstringNoModel name, e.g. gpt-4o-mini. Also used to price the call when you do not send cost_usd.
metadataobjectNoArbitrary key/value pairs (user_id, session_id, etc.)
payloadobjectNoEvent-specific data (see below)
payload.monitor_modebooleanNoMarks the event as observe-only, so the dashboard shows it as mirror-mode traffic. It does not change rule evaluation. The SDKs set it in mirror mode.

ai.request payload

{"messages": [{"role": "user", "content": "Hello"}]}

ai.response payload

{"output": "Hi there!", "usage": {"prompt_tokens": 10, "completion_tokens": 8}, "cost_usd": 0.0018}

An ai.response must carry the request_id of an ai.request you already sent, or it is rejected with 404. Its environment, provider and model are inherited from that request, so you do not need to repeat them.

cost_usd is optional: when you leave it out, SignalVault computes a cost from usage and the model name. Usage may use either OpenAI's prompt_tokens and completion_tokens or Anthropic's input_tokens and output_tokens; both are read for costing and for the daily token budget. Anthropic's prompt cache counts, cache_read_input_tokens and cache_creation_input_tokens, are priced at Anthropic's cache prices. Cache writes are priced at the 5-minute rate unless cache_creation says how many went to the 1-hour cache.

A model SignalVault has no list price for records no cost, rather than a guess: the request shows that the cost is not available, and the Costs page says how many responses its totals exclude. Send cost_usd yourself for an exact figure — always worth doing if you have committed-use pricing, or route through Azure, Bedrock or Vertex, where your rate differs from list price anyway.

agent.tool_call payload

Audit a tool call your agent made. tool_name and duration_ms are required; a payload missing either returns 422 with {"error": {"type": "invalid_payload"}}. Set request_id to tie the call to its parent ai.request, or leave it out for a call with no single parent turn.

FieldTypeRequiredNotes
tool_namestringYes1–200 bytes
duration_msintegerYes0 or greater
tool_inputany JSONNoEncrypted at rest. A hash is stored beside it, so a later change to the stored value can be detected.
tool_outputany JSONNoEncrypted at rest. A hash is stored beside it, so a later change to the stored value can be detected.
errorstringNoUp to 2 000 bytes, when the tool failed
started_atstringNoISO 8601. Falls back to when SignalVault received the event.
{"type": "agent.tool_call", "request_id": "...", "payload": {"tool_name": "fetch_weather", "tool_input": {"city": "Stockholm"}, "tool_output": {"temp_c": 7}, "duration_ms": 142}}

metadata example

Use metadata to attach arbitrary key/value pairs — user IDs, session IDs, feature flags — that appear in the dashboard and in the JSON export:

{"type": "ai.request", "metadata": {"user_id": "u_abc123", "session_id": "sess_xyz", "feature": "chat-v2"}, "payload": {"messages": [{"role": "user", "content": "..."}]}}

Put prompts in the keys SignalVault reads

Guardrails read payload.messages on an ai.request and payload.output on an ai.response. Text under another key is never scanned. Send a prompt as payload.prompt and it is stored unscanned, returning a clean allow because no rule looked at it. Within the messages of a request, guardrails evaluate message text, tool results, and tool inputs and tool-call arguments. Tool inputs and tool-call arguments are read by their string values, for OpenAI and Anthropic alike. Keys and numbers inside tool inputs and tool-call arguments are not evaluated. An arguments string, or a tool input given as a string, that is not valid JSON is evaluated as raw text. A tool input or arguments string that repeats a key is read by value like any other, with every copy of the key included.

Encryption covers a fixed set of keys too: messages and tools on a request, output on a response, the input and output of a tool call, and error, message, detail and description on an ai.error. Anything else is stored as you sent it, in clear. Note also that a tools value that is not a non-empty list is dropped rather than stored.

Responses

The response shape depends on the event type:

Event typeResponse
ai.request Decision + violations + redactions. Use decision to decide whether to proceed with the AI call.
ai.response {"event_id": "...", "status": "recorded", "violations": []} — any post-flight violations are included.
ai.error {"event_id": "..."} — acknowledgement only.
agent.tool_call {"event_id": "...", "decision": "allow", "violations": []} — rules are evaluated against the tool call too.
{"event_id": "...", "decision": "redact", "violations": [...], "redactions": [{"type": "contains_pii", "count": 2}]}

HTTP Status Codes

CodeMeaning
200Accepted. A blocked prompt is also a 200, carrying "decision": "block" — read the decision, not the status.
401Invalid, revoked or missing API key
402subscription_inactive (no plan in service), app_inactive (the app has no active API key), or app_paused (the app is paused, because the plan allows fewer active apps or because it was paused in Account › Billing; the body's upgrade_url is Account › Billing)
403environment_not_allowed — the organization's plan does not permit this key's environment. Every live plan permits all three, so in practice this means the subscription has lapsed.
404{"error": "Request not found"} — an ai.response whose request_id has no matching ai.request
400Body is not valid JSON
413Request body over 4 MB
422Invalid body or payload, or a system-generated event type submitted directly
429Trial limit reached, or rate limited. The bodies differ — see below.
503encryption_unavailable — the event was not stored. Send it again with the same event_id.

Telling the 429s apart

The trial limit returns an object; the rate limits return a bare string, and a request recorded over the rule-check limit adds recorded: true. Check that error is an object before reading error.type:

// Trial limit — HTTP 429
{"error": {"type": "trial_limit_exceeded", "code": "trial_limit_exceeded",
           "message": "Your SignalVault trial has ended. Choose a plan to continue: ...",
           "upgrade_url": "https://signalvault.io/account/billing"}}

// Over the rule-check limit — HTTP 429, with a Retry-After header in seconds.
// The ai.request is recorded, unchecked; do not send it again.
{"error": "rate_limited", "reason": "pre_flight_rate_limited", "recorded": true,
 "event_id": "...", "decision": "unchecked"}

// Over an event ceiling — HTTP 429, with a Retry-After header in seconds.
// Nothing was stored; send the same event again after the delay.
{"error": "rate_limited"}

A lapsed subscription returns 402, not 429, and its body is flat: {"error": "subscription_inactive", "message": "…"}.

Rate limits

EndpointLimitKey
ai.request rule checks120 / 60 sPer app. Over it the request is recorded without a check — see below.
POST /v1/events3,000 events / 60 sPer app, every event type together. A ceiling, not the product limit.
POST /v1/events3,000 events / 60 sPer IP, checked before authentication. A floor against unauthenticated floods.
POST /proxy/*60 req / 60 sPer app

Over the rule-check limit, an ai.request is recorded with the decision unchecked and the reason pre_flight_rate_limited: no rule runs, and only its metadata is stored, never the prompt, since nothing could be redacted from it. The answer is a 429 with recorded: true. Don't send that request again; if you forward it to the provider anyway, send its ai.response as usual, and it is linked to the recorded request. Add "fail_mode": "open" or "closed" to the request's payload and the record also says whether the request was forwarded unchecked.

When a limit is exceeded, SignalVault returns 429 with a Retry-After header in seconds, and retrying after that delay is the right response, except for a request already recorded unchecked. The trial-limit 429 carries no Retry-After, because waiting will not clear it — retrying that one is pointless until the app is subscribed.