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.
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).
| Plan | Requests / month | Price |
free | 10,000 | $0 |
starter | 100,000 | $49 |
pro | 1,000,000 | $199 |
enterprise | unlimited | custom |
# 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.
| Method | Path | What it does |
GET | /api-keys | List this org's License IDs (metadata only) |
POST | /api-keys | Issue a new License ID โ body {"user_id","name?","role?"} (user required) |
POST | /api-keys/{id}/rotate | Renew: 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.
| Role | Can do |
admin | Everything, incl. config & management writes |
manager | Config & management writes, all reads |
viewer | Reads only |
developer | Reads + 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/trends
Historical trend bucketed by day, week or month โ
visitors, gender split, avg age, avg dwell, viewers and avg mood per bucket.
Query params: ?period=day|week|month (default day),
plus the usual ?camera_id= / ?device_id=,
?location_id=, ?from=&to=. With no window it
returns the last ~30 days (day), ~12 weeks (week) or ~6 months (month).
Ideal for daily / weekly / monthly reports and charts.
curl "http://localhost:8000/analytics/trends?period=day&camera_id=HVQA" \
-H "X-API-Key: aa_..."
{
"period": "day", "from": "2026-07-08Tโฆ", "to": null,
"buckets": [
{ "bucket": "2026-08-06", "visitors": 128, "male": 70, "female": 58,
"male_pct": 54.7, "female_pct": 45.3, "avg_age": 31.2,
"avg_dwell_seconds": 22.4, "viewers": 61, "avg_mood": 0.18 }
],
"totals": { "visitors": 128, "viewers": 61, "buckets": 1 }
}
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.
| Code | Meaning |
401 | Missing / invalid / revoked License ID |
403 | Role not permitted, or resource in another organization |
404 | No such resource (e.g. camera) |
413 | Event batch too large (max 1000) |
422 | Invalid body (missing ts/tracking_id, etc.) |