Agentic AI with WebHooks: A Complete Guide to Building Parking Lot Automation

The moment a license plate is caught on camera, the gate opens. No human involvement.

That is the outcome of Agentic AI parking automation. And what makes it possible is not complex AI infrastructure. A single NVR WebHook is enough.

This is a hands-on guide to building, from scratch, a system that uses NOX NVR WebHooks to receive LPR (license plate recognition) events, control the parking gate automatically, and record parking data.

Architecture Overview

The end-to-end flow of the parking automation system is as follows:

[Camera] → [NOX NVR: LPR detection]
              ↓ HTTP POST WebHook
[WebHook server: receive event]
              ↓
[Whitelist DB lookup] → Registered vehicle?
              ↓ YES
[Gate API call] → Open gate
              ↓
[Parking DB record] → Entry/exit time, plate, camera ID
              ↓
[Send notification] → Slack / messenger

Total processing time: within 1–2 seconds of plate recognition. NOX NVR fires the WebHook the instant it detects an event, and developers are free to build whatever logic follows.

Configuring the NOX NVR WebHook

1. Register the WebHook URL

In the NOX NVR admin console, go to Event Settings → WebHook Configuration and register your external server URL.

POST http://your-server.com/webhook/nvr/events
Content-Type: application/json

2. WebHook Payload Structure

The payload NOX NVR sends when an LPR event occurs:

{
  "event_type": "lpr_detected",
  "timestamp": "2026-04-21T09:15:32.418Z",
  "camera_id": "CAM_ENTRANCE_01",
  "camera_name": "Entrance Camera",
  "channel": 1,
  "data": {
    "license_plate": "12가3456",
    "confidence": 0.97,
    "direction": "in",
    "image_url": "http://nvr-host/snapshots/lpr/20260421_091532_CAM01.jpg",
    "crop_image_url": "http://nvr-host/snapshots/lpr/crop/20260421_091532_CAM01.jpg"
  },
  "device_id": "NOX-NVR-SN-20240001",
  "site_id": "SITE_PARKLOT_MAIN"
}

Key fields:

  • event_type: Event type (lpr_detected, motion_detected, line_crossing, etc.)
  • data.license_plate: Recognized license plate
  • data.confidence: Recognition confidence (0–1; 0.85 or higher recommended)
  • data.direction: Entry (in) / exit (out)
  • data.image_url: Full snapshot image URL

Implementation: The WebHook Server

Example Implementation in Python (FastAPI)

from fastapi import FastAPI, Request, HTTPException
from datetime import datetime
import httpx
import sqlite3
import asyncio

app = FastAPI()

# Whitelist DB (use a dedicated database in production)
WHITELIST_DB = "parking.db"
GATE_API_URL = "http://gate-controller.local/api/v1"
SLACK_WEBHOOK = "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK"

@app.post("/webhook/nvr/events")
async def handle_nvr_event(request: Request):
    payload = await request.json()

    # Handle LPR events only
    if payload.get("event_type") != "lpr_detected":
        return {"status": "ignored"}

    plate = payload["data"]["license_plate"]
    confidence = payload["data"]["confidence"]
    direction = payload["data"]["direction"]
    camera_id = payload["camera_id"]

    # Confidence filter (ignore below 85%)
    if confidence < 0.85:
        return {"status": "low_confidence", "plate": plate}

    # Whitelist lookup
    is_registered = check_whitelist(plate)

    if is_registered:
        # Gate control + DB record + notification in parallel
        await asyncio.gather(
            open_gate(direction, camera_id),
            record_parking_event(plate, direction, camera_id, payload["timestamp"]),
            send_slack_notification(plate, direction, camera_id)
        )
        return {"status": "access_granted", "plate": plate}
    else:
        # Record and alert on unregistered vehicle
        await record_unknown_vehicle(plate, camera_id, payload["timestamp"])
        return {"status": "access_denied", "plate": plate}


def check_whitelist(plate: str) -> bool:
    conn = sqlite3.connect(WHITELIST_DB)
    cursor = conn.cursor()
    cursor.execute(
        "SELECT id FROM whitelist WHERE license_plate = ? AND is_active = 1",
        (plate,)
    )
    result = cursor.fetchone()
    conn.close()
    return result is not None


async def open_gate(direction: str, camera_id: str):
    """Call the gate API — controls actual hardware"""
    gate_id = get_gate_id_from_camera(camera_id)
    async with httpx.AsyncClient(timeout=3.0) as client:
        await client.post(
            f"{GATE_API_URL}/gates/{gate_id}/open",
            json={"duration_seconds": 10, "reason": "lpr_access"}
        )


async def record_parking_event(plate: str, direction: str, camera_id: str, timestamp: str):
    """Record parking event in DB"""
    conn = sqlite3.connect(WHITELIST_DB)
    cursor = conn.cursor()
    cursor.execute(
        """INSERT INTO parking_log
           (license_plate, direction, camera_id, event_time, created_at)
           VALUES (?, ?, ?, ?, ?)""",
        (plate, direction, camera_id, timestamp, datetime.utcnow().isoformat())
    )
    conn.commit()
    conn.close()


async def send_slack_notification(plate: str, direction: str, camera_id: str):
    """Send Slack notification"""
    direction_label = "Entry" if direction == "in" else "Exit"
    message = f"🚗 [{direction_label}] {plate} — {camera_id} ({datetime.now().strftime('%H:%M:%S')})"
    async with httpx.AsyncClient() as client:
        await client.post(SLACK_WEBHOOK, json={"text": message})


def get_gate_id_from_camera(camera_id: str) -> str:
    """Map camera ID → gate ID"""
    mapping = {
        "CAM_ENTRANCE_01": "GATE_01",
        "CAM_ENTRANCE_02": "GATE_02",
        "CAM_EXIT_01": "GATE_03",
    }
    return mapping.get(camera_id, "GATE_DEFAULT")

Example Implementation in Node.js (Express)

const express = require('express');
const axios = require('axios');
const Database = require('better-sqlite3');

const app = express();
app.use(express.json());

const db = new Database('parking.db');
const GATE_API_URL = 'http://gate-controller.local/api/v1';

app.post('/webhook/nvr/events', async (req, res) => {
  const { event_type, data, camera_id, timestamp } = req.body;

  if (event_type !== 'lpr_detected') {
    return res.json({ status: 'ignored' });
  }

  const { license_plate: plate, confidence, direction } = data;

  if (confidence < 0.85) {
    return res.json({ status: 'low_confidence', plate });
  }

  const isRegistered = checkWhitelist(plate);

  if (isRegistered) {
    await Promise.all([
      openGate(direction, camera_id),
      recordParkingEvent(plate, direction, camera_id, timestamp),
    ]);
    return res.json({ status: 'access_granted', plate });
  } else {
    return res.json({ status: 'access_denied', plate });
  }
});

function checkWhitelist(plate) {
  const row = db.prepare(
    'SELECT id FROM whitelist WHERE license_plate = ? AND is_active = 1'
  ).get(plate);
  return !!row;
}

async function openGate(direction, cameraId) {
  const gateId = getGateId(cameraId);
  await axios.post(`${GATE_API_URL}/gates/${gateId}/open`, {
    duration_seconds: 10,
    reason: 'lpr_access',
  });
}

function recordParkingEvent(plate, direction, cameraId, timestamp) {
  db.prepare(
    `INSERT INTO parking_log (license_plate, direction, camera_id, event_time)
     VALUES (?, ?, ?, ?)`
  ).run(plate, direction, cameraId, timestamp);
}

function getGateId(cameraId) {
  const mapping = {
    CAM_ENTRANCE_01: 'GATE_01',
    CAM_EXIT_01: 'GATE_02',
  };
  return mapping[cameraId] || 'GATE_DEFAULT';
}

app.listen(8080, () => console.log('WebHook server running on :8080'));

DB Schema

-- Whitelist (registered vehicles)
CREATE TABLE whitelist (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  license_plate TEXT NOT NULL UNIQUE,
  owner_name  TEXT,
  vehicle_type TEXT DEFAULT 'car',
  is_active   INTEGER DEFAULT 1,
  expires_at  TEXT,
  created_at  TEXT DEFAULT (datetime('now'))
);

-- Entry/exit log
CREATE TABLE parking_log (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  license_plate TEXT NOT NULL,
  direction     TEXT NOT NULL,  -- 'in' or 'out'
  camera_id     TEXT NOT NULL,
  event_time    TEXT NOT NULL,
  created_at    TEXT DEFAULT (datetime('now'))
);

-- Unregistered vehicle log
CREATE TABLE unknown_vehicles (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  license_plate TEXT NOT NULL,
  camera_id     TEXT NOT NULL,
  event_time    TEXT NOT NULL,
  image_url     TEXT,
  created_at    TEXT DEFAULT (datetime('now'))
);

Using the NOX NVR REST API

If WebHooks push events in the NVR → external server direction, the REST API provides control in the external server → NVR direction. Used together, they enable full two-way automation.

List Cameras

GET /api/v1/cameras
Authorization: Bearer {api_token}

Sample response:

{
  "cameras": [
    {
      "id": "CAM_ENTRANCE_01",
      "name": "Entrance Camera",
      "channel": 1,
      "status": "online",
      "resolution": "1920x1080",
      "ai_enabled": true,
      "ai_models": ["lpr", "motion"]
    }
  ]
}

Request a Live Snapshot

If you need an additional image when a particular event occurs, you can request a snapshot immediately via the REST API:

GET /api/v1/cameras/CAM_ENTRANCE_01/snapshot
Authorization: Bearer {api_token}

Query Event History

GET /api/v1/events?event_type=lpr_detected&from=2026-04-21T00:00:00Z&limit=100
Authorization: Bearer {api_token}

Operational Tips

1. Handling WebHook Retries

WebHook delivery can fail due to network issues. Design your handler to be idempotent:

@app.post("/webhook/nvr/events")
async def handle_nvr_event(request: Request):
    payload = await request.json()

    # Prevent duplicate processing: dedup by event_id
    event_id = f"{payload['device_id']}_{payload['timestamp']}_{payload['data']['license_plate']}"
    if is_already_processed(event_id):
        return {"status": "duplicate", "event_id": event_id}

    mark_processed(event_id)
    # ... continue processing

2. Handling Gate API Timeouts

If the gate control API call fails, log it and send an alert for manual handling. Return the WebHook response to the NVR immediately and process gate control asynchronously to avoid WebHook timeouts.

@app.post("/webhook/nvr/events")
async def handle_nvr_event(request: Request):
    payload = await request.json()

    # Return immediately (prevents timeouts on the NVR side)
    asyncio.create_task(process_event(payload))
    return {"status": "received"}

3. Tuning the Confidence Threshold

The confidence value varies with camera image quality, lighting, and plate condition. Start at 0.85 and adjust based on real operating data. For night-time operation, you may consider lowering it to 0.80.

Summary

The core of Agentic AI parking automation with NOX NVR WebHooks is simple:

Component Role
NOX NVR + LPR Detects plates and sends WebHooks
WebHook server Receives events, runs business logic
Whitelist DB Manages registered vehicles
Gate API Controls hardware
Parking DB Records entries and exits

With fewer than 100 lines of code, you can connect plate recognition all the way to automatic gate control. If your existing systems already expose a REST API, integration costs drop even further.

Agentic AI is not a grandiose concept. It is implementing a detect → decide → act loop in code. NOX NVR provides the starting point of that loop.


👉 See a live demo in the NOX Vision AI Showroom — watch how a WebHook automation system works in practice

Technical inquiries and POC requests for NOX NVR WebHook integration: yiyolcorp.github.io


Related Posts