Skip to main content

Surveillance

Pi’s surveillance API gives you three surfaces: an async job endpoint for per-frame analysis, a policy management endpoint for reusable behavior rules, and a Server-Sent Events stream for real-time incident delivery.
Keep SSE connections alive and implement automatic reconnection. Pi emits : heartbeat comments every 15 seconds. If your client drops the connection, reconnect with the same query parameters.
Treat video streams as sensitive PII. Use TLS, least-privilege API keys, and enforce data retention policies. Narration is assistive — always verify alerts with operational procedures and applicable local law.

Submit a stream job

POST /api/v1/surveillance/streams Runs perception on a single frame (when input.data is provided), evaluates inline behaviors and stored policies, optionally narrates incidents with Gemini, and persists incidents for SSE subscribers.

Request

curl -sS -X POST "$PI_BASE_URL/api/v1/surveillance/streams" \
  -H "Authorization: Bearer $PI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "stream_id": "cam-entrance-1",
    "source": { "url": "rtsp://camera.local/stream", "type": "rtsp", "fps_cap": 15 },
    "profile": "warehouse_safety",
    "detect": ["person", "forklift"],
    "behaviors": [
      { "type": "loitering", "zone": "loading_dock", "seconds": 90 },
      { "type": "intrusion", "zone": "restricted", "classes": ["person"] }
    ],
    "anomaly": { "enabled": true, "sensitivity": 0.72 },
    "outputs": { "delivery": ["sse", "webhook"], "format": "detailed", "webhook_url": "https://hooks.example.com/alerts" },
    "alerts": { "min_severity": "warning", "cooldown_seconds": 45, "group_by": "zone" },
    "context": {
      "site": "Warehouse 7",
      "zones": {
        "loading_dock": { "xyxy": [0.1, 0.2, 0.5, 0.9] },
        "restricted": { "xyxy": [0.55, 0.1, 0.95, 0.5] }
      }
    },
    "input": { "data": "<base64_or_data_url>", "mime_type": "image/jpeg" },
    "frame_index": 0,
    "output": { "locale": "en", "format": "json" }
  }'

Body parameters

stream_id
string
Optional identifier for the camera or stream. Max 256 characters. Used to scope policies and SSE event filtering.
source
object
Stream source metadata.
profile
string
Built-in behavior preset. One of "retail_security", "warehouse_safety", "smart_city", "residential_perimeter", "construction_site", "parking_lot", "school_campus", "healthcare_facility". Your explicit behaviors override the preset when non-empty.
detect
array
Object class labels to watch for, e.g. ["person", "car", "backpack"]. Max 64 items.
behaviors
array
Inline behavior rules. Supported types: "loitering", "intrusion", "crowd_growth", "object_left", "perimeter_breach", "speed_violation", "wrong_direction". Max 50 rules.Each rule requires type and zone. Additional fields per type:
  • loitering: seconds (1–86400)
  • intrusion: optional classes array
  • crowd_growth: count and window_sec
  • object_left: seconds (1–86400)
  • perimeter_breach: optional boundary ("inside" or "outside")
  • speed_violation: max_speed_mps
  • wrong_direction: allowed_heading_deg and optional tolerance_deg
anomaly
object
Anomaly detection config. Set enabled: true and sensitivity (0–1, default 0.7).
outputs
object
Delivery and format config.
alerts
object
Alert filtering config.
context
object
Arbitrary context including zones (a map of zone id to { xyxy: [x1, y1, x2, y2] } in normalized coordinates). Must serialize to at most 16,000 characters.
input
object
required
Single-frame perception sample.
frame_index
integer
Zero-based index of this frame in the stream. Defaults to 0.

Response

Returns 202 Accepted.
{
  "id": "req_pi_<uuid>",
  "object": "job",
  "status": "queued",
  "created_at": 1711700000,
  "data": { "job_id": "<uuid>" }
}
Poll GET /api/v1/jobs/:id?wait_for_completion=true for data.payload.output containing stream_id, perception results, and incidents.

Create or update a policy

POST /api/v1/surveillance/policies Persist a reusable behavior detection policy scoped to a specific stream or global (when stream_id is empty). Stored policies are automatically evaluated against every stream job for the matching stream_id.

Request

curl -sS -X POST "$PI_BASE_URL/api/v1/surveillance/policies" \
  -H "Authorization: Bearer $PI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "stream_id": "cam-entrance-1",
    "name": "night_perimeter",
    "type": "intrusion",
    "condition": { "zone": "yard" },
    "action": { "severity": "critical", "cooldown_seconds": 120 },
    "enabled": true
  }'

Body parameters

stream_id
string
Target stream. Set to null or omit for a global policy that applies to all streams.
name
string
required
Human-readable policy name. Max 200 characters.
type
string
required
Behavior type. One of "loitering", "intrusion", "crowd_growth", "object_left", "perimeter_breach", "speed_violation", "wrong_direction", "custom".
condition
object
Detection thresholds: zone, duration_seconds, count_threshold, window_sec, speed_threshold_mps, direction_deg, boundary.
action
object
Action on trigger: severity ("info", "warning", "critical"), message_template, cooldown_seconds.
enabled
boolean
Whether the policy is active. Defaults to true.

Subscribe to real-time events (SSE)

GET /api/v1/surveillance/events Opens a persistent text/event-stream connection. Pi emits incident events as JSON and : heartbeat comments every 15 seconds to keep the connection alive.

Query parameters

stream_id
string
Filter events to a specific stream. When set, you also receive global policies (those with no stream_id).
severity
string
Minimum severity filter. One of "info", "warning", "critical".

Consuming SSE events

curl -N "$PI_BASE_URL/api/v1/surveillance/events?stream_id=cam-entrance-1" \
  -H "Authorization: Bearer $PI_API_KEY" \
  -H "Accept: text/event-stream"

SSE event shape

Each incident event body is a JSON object:
{
  "id": "<uuid>",
  "object": "surveillance.incident",
  "stream_id": "cam-entrance-1",
  "type": "intrusion",
  "severity": "critical",
  "created_at": 1711700300,
  "zone": "restricted",
  "detections": [
    { "label": "person", "conf": 0.91, "xyxy": [0.56, 0.12, 0.81, 0.48] }
  ],
  "tracks": [],
  "anomaly_score": null,
  "narration": {
    "summary": "A person entered the restricted storage area.",
    "description": "Detection confidence 91%. Zone: restricted. No prior activity in cooldown window.",
    "recommended_action": "Dispatch security to restricted storage.",
    "confidence": 0.91
  },
  "policy_matched": { "name": "night_perimeter" }
}

Error codes

CodeHTTPDescription
surveillance_disabled403SURVEILLANCE_ENABLED=false in server environment
invalid_request_body400Schema validation failed
job_create_failed500Database insert failed
job_trigger_failed502Background worker not reachable