Five-minute quickstart
From a clean node to the first image event
This golden path needs no camera or cloud account. First use the deterministic mock provider to verify API-key auth, durable resources, JPEG ingest, Events, webhook signatures, and evidence download—then repeat with real RTSP and a real model.
What do you receive? Event output and fields
Watch sends event JSON by HTTP POST to your configured webhook URL. This shortened alert example shows common fields; IDs, time and content are illustrative.
{
"id": "evt_example",
"schema": "sentinel.event.v1",
"type": "monitor.alert.raised",
"severity": "critical",
"state": "raised",
"created_at": "2026-09-07T12:00:00Z",
"source_id": "src_camera03",
"monitor_id": "mon_restricted_area",
"title": "Person entered restricted area",
"summary": "Camera 03 detected a person entering the specified area.",
"evidence": {
"image_url": "/v1/events/evt_example/image"
}
}| Field | Meaning |
|---|---|
| id | Event ID; state updates may share this ID. |
| type | What happened, such as monitor.alert.raised. |
| severity | Severity: safe, suspicious, or critical. |
| state | Lifecycle: raised, updated, or cleared. |
| created_at | Event creation time (ISO 8601). |
| source_id / monitor_id | Source and monitor IDs; may be null for system events. |
| title / summary | Human-readable text; use typed fields for alarm logic. |
| evidence.image_url | Evidence image location, when available; requires an events:read API key. |
Common types include monitor.alert.raised, monitor.alert.updated, monitor.alert.cleared, source.disconnected and inference.failed. Delivery depends on your webhook filters; critical + raised alone does not include all updates or cleared notifications.
Verify the signature, persist the event and return 2xx. Deduplicate retries using the X-Sentinel-Delivery-ID HTTP header, not just the event ID. Webhooks do not deliver every ordinary inference.
local wiring proof
cameras required
node stays running
0. Get the source kit
Run the commands below from a Sentinel Monitor source-kit checkout. There is not yet an anonymously downloadable image or public source repository; the evaluation kit includes the runtime, Compose files, examples, and these docs.
Before running: install and start the prerequisites
Docker supplies the isolated API runtime, mock provider, and durable volumes; the quickstart does not start the optional UI. The macOS/Linux path also needs curl and Python 3 because the script verifies readiness, the first Event, and the evidence hash.
- macOS: install and start Docker Desktop and wait for Engine running.
- macOS: install Python 3 from Python.org. macOS includes curl; if the check cannot find it, finish system updates or use curl's official install guidance. curl ↗
- Ubuntu Linux: install Docker Engine from the official guide and the Compose plugin.
- Ubuntu: an administrator may use sudo apt-get install curl python3 for package installation. After Docker installation, follow the official Linux post-install path (the docker group grants root-equivalent privileges) or use rootless mode, then sign out and back in until docker version works as your normal user. Do not run the Sentinel quickstart with sudo. non-root setup ↗ · rootless ↗
- Windows/WSL: do not install a second Docker Engine inside Ubuntu; use the Windows 11 first-run guide.
docker version
docker compose version
docker info --format '{{.OSType}}'
curl --version
python3 --versionOne command to the first event
macOS, Linux, or Ubuntu/WSL
./deploy/api/quickstart.shWindows PowerShell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\deploy\api\quickstart.ps1Run either command from the source-kit root. The quickstart locates its deployment files, creates private secrets when needed, starts the API-only runtime and deterministic mock provider, waits for readiness, executes the complete first-Event example, and leaves the node running. Neither starts the optional UI. On Windows, ExecutionPolicy applies only to that verified kit's child process; it does not change the user or machine policy.
Advanced: walk the same path manually
1. Start the API-only runtime
cd Sentinel-Monitor/deploy/api
# If .env already exists, keep it; bootstrap refuses to overwrite secrets.
./bootstrap.sh mock
docker compose up -d --build
export SENTINEL_API_KEY="$(python3 ../../docs/api/examples/read_dotenv.py .env SENTINEL_API_KEY)"
export SENTINEL_BASE_URL=http://localhost:8000
curl --fail "$SENTINEL_BASE_URL/readyz"
curl --fail "$SENTINEL_BASE_URL/v1/system" \
-H "Authorization: Bearer $SENTINEL_API_KEY"2. Create a test Source
SOURCE_JSON="$(curl --fail -X POST "$SENTINEL_BASE_URL/v1/sources" \
-H "Authorization: Bearer $SENTINEL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-source-1" \
-d '{
"name": "Quickstart JPEG",
"input": {"type": "frame"}
}')"
export SOURCE_ID="$(printf '%s' "$SOURCE_JSON" |
python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')"
printf 'Created %s\n' "$SOURCE_ID"3. Create a Monitor
MONITOR_JSON="$(python3 -c 'import json,os
print(json.dumps({
"name": "After-hours loading dock",
"source_id": os.environ["SOURCE_ID"],
"prompt": "Notify when a person enters the loading area after hours.",
"enabled": True,
}))' |
curl --fail -X POST "$SENTINEL_BASE_URL/v1/monitors" \
-H "Authorization: Bearer $SENTINEL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-monitor-1" \
--data-binary @-)"
export MONITOR_ID="$(printf '%s' "$MONITOR_JSON" |
python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')"
printf 'Created %s\n' "$MONITOR_ID"4. Optional: register and test a Webhook
: "${SENTINEL_WEBHOOK_URL:?Export a reachable HTTPS receiver URL first}"
WEBHOOK_JSON="$(python3 -c 'import json,os
print(json.dumps({
"name": "My receiver",
"url": os.environ["SENTINEL_WEBHOOK_URL"],
"secret_env": "SENTINEL_WEBHOOK_SIGNING_SECRET",
"enabled": True,
"filters": {"event_types": ["sdk.test"]},
}))' |
curl --fail -X POST "$SENTINEL_BASE_URL/v1/webhook-endpoints" \
-H "Authorization: Bearer $SENTINEL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-webhook-1" \
--data-binary @-)"
export WEBHOOK_ID="$(printf '%s' "$WEBHOOK_JSON" |
python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')"
curl --fail -X POST \
"$SENTINEL_BASE_URL/v1/webhook-endpoints/$WEBHOOK_ID/test" \
-H "Authorization: Bearer $SENTINEL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-webhook-test-1" \
-d '{
"severity": "suspicious",
"title": "Sentinel webhook test",
"summary": "Verify receipt and signature."
}'The automated smoke already verifies exact-body HMAC with a local receiver. Run this manual step only when you have a reachable receiver, and keep it running to receive the sdk.test Event below.
5. Send actual JPEG bytes
export TEST_JPEG="${TMPDIR:-/tmp}/sentinel-quickstart-frame.jpg"
python3 -c 'import base64,pathlib,sys
pathlib.Path(sys.argv[2]).write_bytes(base64.b64decode(pathlib.Path(sys.argv[1]).read_text()))' \
../../docs/api/assets/sample-frame.jpg.b64 "$TEST_JPEG"
curl --fail -X POST "$SENTINEL_BASE_URL/v1/sources/$SOURCE_ID/frames" \
-H "Authorization: Bearer $SENTINEL_API_KEY" \
-H "Content-Type: image/jpeg" \
-H "Idempotency-Key: quickstart-frame-1" \
--data-binary "@$TEST_JPEG"frames is a bounded test/custom-ingest path, not a video-storage API. The runtime validates media type, decoded dimensions, and size limits before inference.
6. Create a deterministic Event with image evidence
EVENT_JSON="$(python3 -c 'import base64,json,os,pathlib
print(json.dumps({
"source_id": os.environ["SOURCE_ID"],
"monitor_id": os.environ["MONITOR_ID"],
"type": "sdk.test",
"severity": "suspicious",
"summary": "Quickstart evidence event",
"image_base64": base64.b64encode(pathlib.Path(os.environ["TEST_JPEG"]).read_bytes()).decode(),
}))' |
curl --fail -X POST "$SENTINEL_BASE_URL/v1/events/test" \
-H "Authorization: Bearer $SENTINEL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-event-1" \
--data-binary @-)"
export EVENT_ID="$(printf '%s' "$EVENT_JSON" |
python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')"
printf 'Created %s\n' "$EVENT_ID"image_base64 exists only on the controlled test-event endpoint so a camera-free setup can validate evidence. Normal Events and webhooks return image_url and do not inline Base64 by default.
7. Query the Event and download the JPEG
curl --fail "$SENTINEL_BASE_URL/v1/events/$EVENT_ID" \
-H "Authorization: Bearer $SENTINEL_API_KEY"
curl --fail "$SENTINEL_BASE_URL/v1/events/$EVENT_ID/image" \
-H "Authorization: Bearer $SENTINEL_API_KEY" \
--output event.jpg
file event.jpgOrdinary inference text and live preview
curl --no-buffer --fail \
"$SENTINEL_BASE_URL/v1/analyses/stream?monitor_id=$MONITOR_ID" \
-H "Authorization: Bearer $SENTINEL_API_KEY" \
-H "Accept: text/event-stream"
curl --fail \
"$SENTINEL_BASE_URL/v1/sources/$SOURCE_ID/stream?max_fps=15&max_height=720" \
-H "Authorization: Bearer $SENTINEL_API_KEY" \
--output preview.mjpegThe first stream emits every completed ordinary inference, including safe results; the second is the bounded MJPEG preview. Use /v1/events, /v1/events/stream, or signed Webhooks for alerts, warnings, and errors. Closing the desktop window does not stop these API outputs.
Python: run the same tested flow
The source kit includes a complete Python 3 standard-library example. It creates a Source and Monitor, ingests a JPEG, creates a test Event, and verifies the downloaded evidence SHA-256.
cd deploy/api
export SENTINEL_BASE_URL=http://127.0.0.1:8000
export SENTINEL_API_KEY="<your-server-side-key>"
python3 ../../docs/api/examples/first_event.pyJavaScript: query and binary image
import { writeFile } from "node:fs/promises";
const base = "http://localhost:8000";
const headers = { Authorization: `Bearer ${process.env.SENTINEL_API_KEY}` };
const events = await fetch(`${base}/v1/events?limit=10`, { headers })
.then(async (response) => {
if (!response.ok) throw await response.json();
return response.json();
});
const event = events.data[0];
if (!event) throw new Error("No events returned");
const image = await fetch(new URL(event.evidence.image_url, base), { headers });
if (!image.ok) throw new Error(`image download failed: ${image.status}`);
await writeFile("event.jpg", Buffer.from(await image.arrayBuffer()));Move to real input
- Select local_* or private_openai_compatible from the model guide; use cloud_gemini only after its documented release gate passes.
- Configure the exact RTSP host/host:port or CIDR allowlist first, then create a Source with input.type=rtsp.
- Call Source test and verify a decodable frame or sanitized failure code.
- Create a new Monitor linked to the RTSP Source, then trigger real inference with representative footage; the mock Monitor remains linked to the frame Source.