Veto/docs

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

TypePayload fieldsEmitted when
decision.createdtoolName, decision, mode, latencyMsA tool call is validated and a decision is recorded

Approvals

TypePayload fieldsEmitted when
approval.createdapprovalId, toolName, decisionIdA tool call triggers require_approval and an approval request is created
approval.resolvedapprovalId, action, resolvedByA pending approval is approved or denied (single or bulk)

Policies

TypePayload fieldsEmitted when
policy.createdtoolName, projectIdA new policy is created
policy.updatedtoolName, projectId, versionAn existing policy is updated
policy.activatedtoolName, projectIdA policy is activated
policy.deactivatedtoolName, projectIdA policy is deactivated
policy.deletedtoolName, projectIdA policy is deleted

Drafts

TypePayload fieldsEmitted when
draft.submitteddraftId, orgIdA policy draft is submitted for review
draft.approveddraftIdA policy draft is approved
draft.rejecteddraftIdA 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/stream

Authentication

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.created

Connection behavior

  • The server sends :ok immediately on connect to confirm the stream is open.
  • A :ping comment is sent every 15 seconds to keep the connection alive through proxies and load balancers.
  • Events are formatted as standard SSE: event: <type> followed by data: <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 ID
  • iss — auth service URL
  • audapi.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 ping frame every 30 seconds. Clients that fail to respond with pong are 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

ScenarioTransportWhy
Agent reacting to approval decisionsSSEAPI-key auth, no browser needed, subscribeEvents async iterator fits agent loops
Dashboard showing live decisionsWebSocketJWT auth, org-scoped routing, bidirectional for future features
CI/CD pipeline waiting for approvalSSEsubscribeEvents with a break on approval — clean exit
Monitoring policy changes across orgsWebSocketSingle connection covers all user orgs automatically
Serverless function / LambdaSSE (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.