Real-Time Events
Stream policy decisions, approvals, and changes over WebSocket or SSE.
Veto emits real-time events whenever policies change, approvals are resolved, or decisions are made. Two transport options are available: Server-Sent Events (SSE) for agents and scripts, and WebSocket for dashboards and interactive UIs.
Both transports deliver the same events with the same payloads. They differ in auth mechanism and connection model.
Event types
Every event has a type string and a data object. The full set:
Decisions
| Type | Payload fields | Emitted when |
|---|---|---|
decision.created | toolName, decision, mode, latencyMs | A tool call is validated and a decision is recorded |
Approvals
| Type | Payload fields | Emitted when |
|---|---|---|
approval.created | approvalId, toolName, decisionId | A tool call triggers require_approval and an approval request is created |
approval.resolved | approvalId, action, resolvedBy | A pending approval is approved or denied (single or bulk) |
Policies
| Type | Payload fields | Emitted when |
|---|---|---|
policy.created | toolName, projectId | A new policy is created |
policy.updated | toolName, projectId, version | An existing policy is updated |
policy.activated | toolName, projectId | A policy is activated |
policy.deactivated | toolName, projectId | A policy is deactivated |
policy.deleted | toolName, projectId | A policy is deleted |
Drafts
| Type | Payload fields | Emitted when |
|---|---|---|
draft.submitted | draftId, orgId | A policy draft is submitted for review |
draft.approved | draftId | A policy draft is approved |
draft.rejected | draftId | A policy draft is rejected |
SSE (Server-Sent Events)
SSE is the best choice for agents, CLI tools, and CI/CD pipelines. The connection is a long-lived HTTP GET that the server pushes events into.
Endpoint
GET /v1/events/streamAuthentication
Pass your API key via header or query parameter:
- Header:
X-Veto-API-Key: veto_sk_... - Query:
?apiKey=veto_sk_...
Filtering
Use the types query parameter to receive only specific event types (comma-separated). If omitted, all events for your organization are delivered.
GET /v1/events/stream?types=approval.resolved,decision.createdConnection behavior
- The server sends
:okimmediately on connect to confirm the stream is open. - A
:pingcomment is sent every 15 seconds to keep the connection alive through proxies and load balancers. - Events are formatted as standard SSE:
event: <type>followed bydata: <json>.
curl example
curl -N -H "X-Veto-API-Key: veto_sk_..." \
"https://api.veto.so/v1/events/stream?types=approval.resolved"Output:
:ok
event: approval.resolved
data: {"approvalId":"apr_abc123","action":"approve","resolvedBy":"user_xyz"}
:ping
event: approval.resolved
data: {"approvalId":"apr_def456","action":"deny","resolvedBy":"user_xyz"}SDK — onEvent (callback)
onEvent opens an SSE connection and calls your callback for each matching event. Returns an EventSubscription with an unsubscribe() method.
import { VetoAdmin } from "veto";
const admin = new VetoAdmin({
apiKey: "veto_sk_...",
baseUrl: "https://api.veto.so",
});
const sub = admin.onEvent("approval.resolved", (event) => {
console.log(event.type, event.data);
});
// Later:
sub.unsubscribe();You can pass an array to listen for multiple types:
const sub = admin.onEvent(
["approval.resolved", "decision.created"],
(event) => {
// handle event
}
);SDK — subscribeEvents (async iterator)
subscribeEvents returns an AsyncIterable that yields events as they arrive. This is the natural fit for for await loops in agent code.
for await (const event of admin.subscribeEvents({
types: ["approval.resolved"],
})) {
if (event.data.action === "approve") {
console.log("Approval granted:", event.data.approvalId);
break;
}
}WebSocket
WebSocket is the best choice for dashboards and real-time UIs where you need bidirectional communication and org-scoped event routing.
Endpoint
ws://api.veto.so/ws?token=<JWT>Authentication
Pass a JWT as the token query parameter. The token must be issued by the Veto auth service and include:
sub— user IDiss— auth service URLaud—api.veto.so
On connect, the server resolves all organizations the user belongs to and delivers events for all of them.
Connection behavior
- The server sends a
pingframe every 30 seconds. Clients that fail to respond withpongare terminated on the next heartbeat cycle. - Events arrive as JSON text frames:
{"type": "decision.created", "data": {...}}. - The connection is scoped to the authenticated user's organizations — no additional subscription message is needed.
Browser example
const ws = new WebSocket(
`wss://api.veto.so/ws?token=${jwt}`
);
ws.onmessage = (msg) => {
const event = JSON.parse(msg.data);
console.log(event.type, event.data);
};
ws.onclose = () => {
// reconnect logic
};When to use which
| Scenario | Transport | Why |
|---|---|---|
| Agent reacting to approval decisions | SSE | API-key auth, no browser needed, subscribeEvents async iterator fits agent loops |
| Dashboard showing live decisions | WebSocket | JWT auth, org-scoped routing, bidirectional for future features |
| CI/CD pipeline waiting for approval | SSE | subscribeEvents with a break on approval — clean exit |
| Monitoring policy changes across orgs | WebSocket | Single connection covers all user orgs automatically |
| Serverless function / Lambda | SSE (short-lived) | Open connection, wait for specific event, close |
Architecture notes
Both transports share a Redis pub/sub backbone (ws:events channel). When the server runs as a single instance without Redis, events are delivered directly in-process. In a multi-instance deployment, Redis ensures events published on one instance reach SSE and WebSocket clients connected to any other instance.