Audience Analytics
Dashboard
Developer Guide

API Documentation

The analytics engine for audience data. Your cameras detect people; we turn that into people counts, demographics, dwell, viewing, and live occupancy โ€” served over a clean REST API you consume from your own dashboards. We provide your base URL and License ID โ€” there is nothing to install and no codebase to run on your side.

Base URL http://localhost:8000 Swagger /docs Admin dashboard

Authentication

Every endpoint except /health requires a License ID tied to one organization โ€” all data is isolated per organization. Send your License ID three ways (the transport names X-API-Key / api_key are the HTTP field names; the value is always your License ID):

curl http://localhost:8000/analytics/people \
  -H "X-API-Key: aa_your_license_id"

# or a bearer token
  -H "Authorization: Bearer aa_your_license_id"

# or a query param (handy in a browser)
http://localhost:8000/analytics/people?api_key=aa_your_license_id

You don't create License IDs yourself โ€” we issue the License ID and share it with you. You need nothing else: no software to install, no codebase, no database. Just use the License ID we provide, exactly as shown above. If you don't have a License ID yet, contact us to get one.

About your License ID

  • The License ID we provide is shown once, at creation โ€” store it safely. We keep only a hash, so it can never be shown again.
  • License IDs do not expire โ€” they stay valid until revoked.
  • Lost or leaked a License ID? Re-create a new one and revoke the old. There is no recovery of the original (renew = new License ID + revoke old).
  • Usage is metered against your plan's monthly request quota (see Billing & Usage below). Over quota returns 429. No real card charges yet.

Billing & usage

Each organization is on a plan with a monthly request quota. Every authenticated request is metered; once you exceed the quota, metered endpoints return 429 until you upgrade or the month resets (the billing/usage endpoints themselves stay reachable so you can upgrade).

PlanRequests / monthPrice
free10,000$0
starter100,000$49
pro1,000,000$199
enterpriseunlimitedcustom
# your plan + usage this month
curl http://localhost:8000/usage -H "X-API-Key: aa_your_license_id"

# available plans
curl http://localhost:8000/billing/plans -H "X-API-Key: aa_your_license_id"

# change plan (admin/manager)
curl -X PUT http://localhost:8000/billing/plan \
  -H "X-API-Key: aa_admin_license_id" -H "Content-Type: application/json" \
  -d '{"plan":"pro"}'

Managing License IDs (admin only โ€” optional)

Most customers can ignore this section โ€” you just use the License ID we gave you. Only if you were issued an admin License ID can you also issue, list, renew and revoke License IDs for your own organization over the API. Newly created and renewed License IDs return the raw License ID once.

MethodPathWhat it does
GET/api-keysList this org's License IDs (metadata only)
POST/api-keysIssue a new License ID โ€” body {"user_id","name?","role?"} (user required)
POST/api-keys/{id}/rotateRenew: new License ID with the same name + role, old one revoked
DELETE/api-keys/{id}Revoke a License ID immediately
# issue a License ID for a device / customer
curl -X POST http://localhost:8000/api-keys \
  -H "X-API-Key: aa_admin_license_id" -H "Content-Type: application/json" \
  -d '{"user_id":18,"name":"device: Shop-01","role":"developer"}'

# renew (rotate) it โ€” returns a new License ID, revokes the old one
curl -X POST http://localhost:8000/api-keys/12/rotate \
  -H "X-API-Key: aa_admin_license_id"

# revoke a License ID
curl -X DELETE http://localhost:8000/api-keys/12 \
  -H "X-API-Key: aa_admin_license_id"

Roles

License IDs carry a role. Reads accept any valid License ID; sending events needs developer, manager or admin (a read-only viewer is rejected); configuration and management writes require admin or manager.

RoleCan do
adminEverything, incl. config & management writes
managerConfig & management writes, all reads
viewerReads only
developerReads + event ingestion

Ingestion

POST/events

Ingest one event or a batch (max 1000). The camera auto-registers to your org on first sight. Each event needs at least ts (unix seconds) and tracking_id.

curl -X POST http://localhost:8000/events \
  -H "X-API-Key: aa_..." -H "Content-Type: application/json" \
  -d '{"events":[{
    "ts": 1753600000, "camera_id": "cam-01", "tracking_id": "cam-01:p1",
    "age": 30, "gender": "male", "dwell_seconds": 12.4,
    "viewing": true, "viewing_seconds": 5.1, "confidence": 0.92
  }]}'
{ "ok": true, "count": 1 }

Analytics

All analytics endpoints accept an optional time window ?from=&to= (ISO timestamps) and a noise filter ?min_events=&min_dwell=. A track only counts once it clears the filter (โ‰ฅ min_events detections OR โ‰ฅ min_dwell seconds) โ€” this drops 1โ€“2 frame ghost tracks so they don't inflate counts.

They also accept scope filters โ€” ?camera_id= (one camera; ?device_id= works as an alias), ?location_id= (all cameras at a location), and on people/demographics ?content_id= (only while that ad/creative played). Omit them for the whole organization. /analytics/people also takes ?extrapolate=true to add an estimated_reach. To pull every metric for one device at once, use GET /cameras/{camera_id}/analytics.

GET/analytics/people

Footfall & unique visitors. total_unique_people counts distinct people; total_visits counts visits (a person who leaves and returns after a gap is one person but a new visit); repeat_visitors is how many of those people came back more than once. Same split per hour under by_hour.

{
  "total_unique_people": 3,
  "total_visits": 4,
  "repeat_visitors": 1,
  "by_hour": [{ "hour": "2026-07-27T12:00:00+05:30", "people": 3, "visits": 4 }],
  "filter": { "min_events": 5, "min_dwell_seconds": 1.5 }
}
GET/analytics/demographics

Gender split + age distribution. Age is the median per person (robust to per-frame jitter); gender is the per-person mode. mask breaks down how many wore a face mask, and avg_confidence (0..1) is how much to trust the age/gender (masked / off-angle faces score low). age_by_gender gives the age groups split by gender โ€” female and male as separate distributions (0-10 โ€ฆ 70+), never combined. Query params: ?viewers_only=true (only people who looked at the screen) and ?min_confidence=0.5 (drop low-trust masked / off-angle guesses). All accept ?camera_id= / ?device_id=, ?location_id=.

{
  "gender_split": { "male": 1, "female": 2, "unknown": 0 },
  "gender_pct": { "male": 33.3, "female": 66.7, "unknown": 0.0 },
  "age_distribution": { "18-24": 1, "25-34": 2 },
  "age_brackets": { "21-30": 2, "31-40": 1 },
  // age groups split by gender โ€” female vs male, counted separately
  "age_by_gender": {
    "female": { "0-10": 0, "21-30": 2, "31-40": 0, "70+": 0 },
    "male":   { "0-10": 0, "31-40": 1, "51-60": 0, "70+": 0 }
  },
  "avg_age": 24.3,
  "mask": { "masked": 1, "unmasked": 2 },
  "glasses": { "with_glasses": 1, "without_glasses": 2 },
  "avg_confidence": 0.82,
  "avg_mood": 0.31,                // -1 negative .. +1 positive
  "emotion_distribution": { "happy": 2, "neutral": 1, "sad": 0 },
  "viewers_only": false
}
GET/analytics/emotion

Facial-expression breakdown of the audience โ€” how many people read as happy (smiling), neutral, sad, angry, surprise, fear or disgust. Counted per person (each visitor's dominant expression across their frames), so a lingering smile isn't counted a hundred times. Returns per-emotion counts and percentages, an average mood valence (โˆ’1 negative .. +1 positive) with a mood_label, the dominant_emotion, a smiling headline (count + rate), a positive / neutral / negative sentiment rollup, and an hourly by_hour mood + smile trend. Same scoping as demographics: ?camera_id= / ?device_id=, ?location_id=, ?content_id=, ?viewers_only=true, ?from=&to=.

curl "http://localhost:8000/analytics/emotion?camera_id=entrance" \
  -H "X-API-Key: aa_..."

{
  "people": 6,
  "avg_mood": 0.17, "mood_label": "positive",
  "dominant_emotion": "happy",
  "emotion_distribution": { "happy": 2, "surprise": 1, "neutral": 1, "sad": 1, "angry": 1, "fear": 0, "disgust": 0 },
  "emotion_pct": { "happy": 33.3, "neutral": 16.7, โ€ฆ },
  "smiling": { "count": 2, "rate": 33.3 },
  "sentiment": { "positive": 3, "neutral": 1, "negative": 2 },
  "sentiment_pct": { "positive": 50.0, "neutral": 16.7, "negative": 33.3 },
  "by_hour": [ { "hour": "2026-08-07T18:00", "avg_mood": 0.2, "happy": 12, "faces": 36 } ]
}
GET/analytics/dwell

Dwell-time stats across qualified visitors.

{ "people": 3, "avg_dwell_seconds": 20.6, "min_dwell_seconds": 7.1, "max_dwell_seconds": 37.1 }
GET/analytics/viewing

Attention (opportunity-to-see): how many present people actually looked at the screen (head pose), the viewer rate, viewing time, and the demographics of the viewers โ€” who looked, not who merely walked past.

{
  "people": 3, "viewers": 1, "viewer_rate": 0.333,
  "avg_viewing_seconds": 4.1, "avg_dwell_seconds": 6.0,
  "viewer_gender_split": { "male": 0, "female": 1 },
  "avg_viewer_age": 28.0
}
GET/analytics/live

Who is in front of the camera right now (default window 10s). Optional ?window= seconds and ?camera_id=.

{
  "current_people": 1, "current_viewers": 1, "crowded": false,
  "people": [{ "tracking_id": "cam-01:p1", "gender": "female", "age": 25, "viewing": true }],
  "cameras": [{ "camera_id": "cam-01", "online": true }]
}
GET/analytics/occupancy

Live occupancy vs the crowd threshold.

{ "occupancy": 0, "threshold": 3, "status": "normal", "crowded": false }
GET/analytics/rollup/hourly

Precomputed hourly rollup for fast dashboard queries. Refresh it with POST /admin/rollups/refresh (admin/manager).

{ "hourly": [{ "hour": "2026-07-27T12:00:00+05:30", "camera_id": "cam-01", "people": 3, "avg_dwell": 10.5 }] }
GET/analytics/content

Per-content attribution โ€” impressions (people who looked), view rate and attention per ad/creative that was playing (from each event's content_id). The core DOOH monetisation metric.

{ "content": [{ "content_id": "ad-nike", "impressions": 2, "people": 2, "view_rate": 1.0, "avg_attention_seconds": 3.0 }] }
GET/feed/impressions

Programmatic-DOOH audience feed: per-screen opportunity-to-see, impressions, and an impression_multiplier (the camera's extrapolation factor โ†’ estimated reach).

{ "screens": [{ "screen_id": "cam-01", "opportunity_to_see": 3, "impressions": 2, "impression_multiplier": 2.0, "estimated_impressions": 4 }] }

Cameras

Connecting a camera

Any device connects the same way โ€” an edge box, a phone app, or a browser. The device authenticates with an License ID, registers a camera to get its camera id, then streams. There are two modes:

  • Device detects, sends events โ€” the device runs detection and posts results to /events. Scales to thousands of cameras.
  • Device sends frames, we detect โ€” the device streams JPEG frames and our server runs the analysis. Simplest to integrate.
POST/camera/session/start admin / manager

Register/name a camera and start its analyzer. Returns the camera_id used for everything after.

curl -X POST http://localhost:8000/camera/session/start \
  -H "X-API-Key: aa_..." -H "Content-Type: application/json" \
  -d '{"name": "Entrance - Main Store"}'
# โ†’ {"camera_id": "entrance-main-store", ...}
POST/cameras/{camera_id}/device admin / manager / developer

Report the device details & location captured on connect โ€” model, GPS latitude/longitude, and any extra info. Shown in the dashboard.

curl -X POST http://localhost:8000/cameras/entrance-main-store/device \
  -H "X-API-Key: aa_..." -H "Content-Type: application/json" \
  -d '{"device_name": "Galaxy A54", "latitude": 12.9716, "longitude": 77.5946}'
POST/camera/frame?camera_id={id} admin / manager / developer

Frame mode โ€” send the raw JPEG bytes of each frame (~5 fps, 640ร—480). The server detects people, age, gender, dwell and viewing.

curl -X POST "http://localhost:8000/camera/frame?camera_id=entrance-main-store" \
  -H "X-API-Key: aa_..." -H "Content-Type: image/jpeg" \
  --data-binary @frame.jpg
POST/camera/session/stop admin / manager

Stop the camera's analyzer when the device disconnects.

Register & pair a device

Register a device once; the owner gets a pairing code (unique, single-use, does not expire). The device app enters that code to claim the existing camera and receive its key โ€” no keys typed, no duplicate cameras.

POST/devices admin / manager

Register a device under your org (no new key minted). Returns the camera_id + a pairing_code.

curl -X POST http://localhost:8000/devices \
  -H "X-API-Key: aa_..." -H "Content-Type: application/json" \
  -d '{"name": "Entrance - Main Store", "device_code": "YE7F"}'
# โ†’ {"camera_id": "entrance-main-store", "pairing_code": "E7JV46GV"}
POST/devices/pair no key needed

The app claims a device by its code โ€” the code IS the credential (single-use). Returns the existing camera's id, location, org and the org api_key.

curl -X POST http://localhost:8000/devices/pair \
  -H "Content-Type: application/json" -d '{"code": "E7JV46GV"}'
POST/cameras/{id}/pair-code admin / manager

Issue a fresh pairing code (e.g. after the old one was claimed).

POST/cameras/{id}/health admin / manager / developer

A device reports image quality {blur, brightness}; the API derives a status (good / blurry / dark / washed_out / covered).

Managing cameras

GET/cameras

List cameras for your org โ€” id, name, location, device code, device, GPS, live/offline status and last-seen.

GET/cameras/{camera_id}

One device's full record plus a quick stats summary โ€” people seen, visits, events, and how many are in view right now.

{
  "id": "usb-camera", "name": "Front Entrance", "pair_code": "HVQA",
  "location_name": "Main Store", "online": true, "health_status": "good",
  "stats": { "unique_people": 63, "visits": 63, "events": 13599, "current_people": 9 }
}

device_id is accepted everywhere as an alias for camera_id โ€” the response also echoes both.

GET/cameras/{camera_id}/analytics

Everything for one device in a single call. Bundles people, demographics, dwell, viewing, attention, live, occupancy and content, all scoped to this device. Accepts the same ?from=&to= window. {camera_id} may be the device_id too. Saves you several round-trips when a dashboard opens a single device. A metric that can't compute comes back as {"error": โ€ฆ} instead of failing the whole payload.

curl "http://localhost:8000/cameras/usb-camera/analytics" \
  -H "X-API-Key: aa_..."

{
  "device_id": "usb-camera", "camera_id": "usb-camera",
  "window": { "from": null, "to": null },
  "people": { "total_unique_people": 63, "total_visits": 71, โ€ฆ },
  "demographics": { "gender": {โ€ฆ}, "age": {โ€ฆ}, "avg_mood": 0.31, โ€ฆ },
  "dwell": {โ€ฆ}, "viewing": {โ€ฆ}, "attention": {โ€ฆ},
  "live": {โ€ฆ}, "occupancy": {โ€ฆ}, "content": {โ€ฆ}
}
PUT/cameras/{camera_id} admin / manager

Rename a device, assign a location, set a custom device_code, or set the extrapolation_factor. The camera_id itself is fixed โ€” the name is a label you can change anytime.

curl -X PUT http://localhost:8000/cameras/cam-01 \
  -H "X-API-Key: aa_..." -H "Content-Type: application/json" \
  -d '{"name": "Front Entrance", "location_id": 1, "device_code": "HVQA"}'

Management โ€” Locations

GET/locations

List locations (each with a camera count and timezone).

POST/locations admin / manager
curl -X POST http://localhost:8000/locations \
  -H "X-API-Key: aa_..." -H "Content-Type: application/json" \
  -d '{"name": "Front Store", "timezone": "Asia/Kolkata"}'

Alerts & config

GET/alerts

Recent crowd threshold-crossing alerts for your org.

GET/config/alerts  PUT/config/alerts admin / manager

View or change the crowd threshold, live window, and webhook URL.

curl -X PUT http://localhost:8000/config/alerts \
  -H "X-API-Key: aa_..." -H "Content-Type: application/json" \
  -d '{"threshold": 5, "webhook_url": "https://you.example.com/hook"}'

CSV export

GET/export/events.csv
GET/export/hourly.csv

Download raw events or the hourly rollup as CSV. Accepts the same ?from=&to= window.

curl "http://localhost:8000/export/hourly.csv?from=2026-07-27T00:00:00" \
  -H "X-API-Key: aa_..." -o hourly.csv

Webhooks

Subscribe your own URL to events with POST /webhooks ({"url": "...", "events": [...]}). Every delivery is signed โ€” verify the X-AA-Signature header (HMAC-SHA256 of the body with your secret). Available event types: crowd_alert, crowd_cleared, daily_summary, viewer_started, viewer_ended.

Crowd alert (occupancy crosses the threshold):

{
  "type": "crowd_alert",           // or "crowd_cleared"
  "org_id": 1,
  "occupancy": 5,
  "threshold": 3,
  "ts": "2026-07-27T12:11:05+00:00"
}

Viewer engagement โ€” fired in real time the moment a person starts and stops looking at the screen (the important audience). viewer_ended carries how long they looked; reason: "left_frame" means they walked off rather than looked away:

{
  "type": "viewer_started",        // someone began looking
  "org_id": 1, "tracking_id": "dash-live:p1#1", "camera_id": "dash-live",
  "gender": "female", "age": 27,
  "ts": "2026-07-27T12:43:24+00:00"
}
{
  "type": "viewer_ended",          // they stopped looking
  "org_id": 1, "tracking_id": "dash-live:p1#1", "camera_id": "dash-live",
  "gender": "female", "age": 27, "viewing_seconds": 143.4,
  "reason": "left_frame",        // present only on a timeout end
  "ts": "2026-07-27T12:45:47+00:00"
}

Errors

Standard HTTP status codes with a JSON {"detail": "..."} body.

CodeMeaning
401Missing / invalid / revoked License ID
403Role not permitted, or resource in another organization
404No such resource (e.g. camera)
413Event batch too large (max 1000)
422Invalid body (missing ts/tracking_id, etc.)

Audience Analytics โ€” POC API reference. Every feature is API-first; the admin dashboard consumes exactly these endpoints. For the live interactive schema see /docs.