Pushing face detections
eConnect's own face recognition writes its sightings into Object Analytics — and so can yours. Push detections from your engine and every downstream feature works on them: subjects and their history, face tags and watch lists, alerts, duration of visit, semantic search, and the video beside each hit.
POST /api/v2/oa/plugins/{pluginInstalledId}/subject-detections
The body is an array, so this is the batch endpoint as well as the single one. It returns true
on success.
Register a detector first
A detector is the thing that did the seeing — your camera, engine or sensor, as eConnect knows it. Every detection names its detector by familiar id, and a detection whose detector does not exist has nowhere to land. Create it once, at install time:
PUT /api/v2/oa/plugins/{pluginInstalledId}/subject-detectors
{
"familiarId": "acme-fr-door-3",
"familiarName": "North Entrance — Door 3",
"detectorType": "Acme FR 2.1",
"timeZone": "America/New_York",
"isDisabled": false
}
| Field | What it is |
|---|---|
familiarId | Your stable identifier. This is the value every detection carries — pick it once and never change it |
familiarName | What operators see in grids and filters |
detectorType | Your engine's name and version. Useful when several engines feed one site |
timeZone | IANA zone (America/New_York), so local-time reporting is right regardless of where the server sits |
List what exists with GET .../subject-detectors, map your ids to eConnect's once at start-up, and
fail loudly on anything unmapped rather than sending detections into a void.
If you register a detector twice — a rename, a re-install — an administrator can merge the duplicate
into the real one with POST .../subject-detectors/{targetDetectorId}/merge, and the detections move
with it. Nothing is lost, but it is work; a stable familiarId avoids it.
The payload
[
{
"subject": "8f2b6d41-0c3e-4a7a-9d55-1f3f6a2b9e10",
"subjectRef": "d41d8cd9-8f00-3204-a980-0998ecf8427e",
"device": "acme-fr-door-3",
"dataTimeStamp": "2026-10-02T21:14:05Z",
"timeZone": "America/New_York",
"detectorType": "Acme FR 2.1",
"confidence": 0.95,
"age": 41,
"gender": "M"
}
]
| Field | Why it matters |
|---|---|
subject | Who was seen — the subject identity eConnect holds. A detection with no subject is an unidentified sighting; with one it joins that person's history |
subjectRef | This sighting. Your engine's own immutable detection id, unique per observation. It is the key the close call below uses, and what makes a resend traceable |
device | The detector's familiarId — in practice the camera the face was seen on |
dataTimeStamp | When the face was seen, UTC. Everything time-based — history, alerts, the jump to video — works from this |
timeZone | The detector's IANA zone |
detectorType | The engine that made the match |
confidence | Match confidence, 0–1. Operators filter on it, so send the real number rather than a constant |
age | Estimated age, where your engine produces one |
gender | A single character: M, F, or U for unknown |
Demographics feed the demographics view. Send U rather than a guess, and leave age out if your
engine is not estimating it — an invented number is worse than a missing one, because a report will
believe it.
Getting the subject id
Subjects are people, and they live in eConnect. Search for one, or create one, before you can attribute a detection to it:
GET /api/v2/oa/plugins/{pluginInstalledId}/subjects/search?identification=...&maxRecords=10
GET /api/v2/oa/plugins/{pluginInstalledId}/subjects/by-face/{faceId}
PUT /api/v2/oa/plugins/{pluginInstalledId}/subjects
If your engine has its own gallery, create the subject once per person and cache the mapping. If it
does not, push the detection without a subject — it is still a sighting on a timeline, still
searchable, and an operator can identify it later.
Closing an observation
A face that lingers in frame is one observation, not two hundred. Insert the detection when the subject appears, then close the segment when they leave, and eConnect derives the duration from the two timestamps — this is what fills in duration of visit for an operator:
POST /api/v2/oa/plugins/{pluginInstalledId}/subject-detections/close
[
{ "subjectRef": "d41d8cd9-8f00-3204-a980-0998ecf8427e", "lastDetectionUtc": "2026-10-02T21:16:48Z" }
]
It is keyed on subjectRef and returns the number of rows updated.
The close call can legitimately return 0: the row may not have landed yet, or it may have been
purged by retention. Treat zero as information, not as an error to retry forever.
Sending it
curl -X POST \
"https://your-server/api/v2/oa/plugins/$OA_ID/subject-detections" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '[
{
"subject": "8f2b6d41-0c3e-4a7a-9d55-1f3f6a2b9e10",
"subjectRef": "d41d8cd9-8f00-3204-a980-0998ecf8427e",
"device": "acme-fr-door-3",
"dataTimeStamp": "2026-10-02T21:14:05Z",
"timeZone": "America/New_York",
"confidence": 0.95
}
]'
// An engine streams hits; batch what arrived in the last second or two.
var batch = hits.Select(h => new
{
subject = h.GalleryMatch?.EConnectSubjectId, // null for an unidentified sighting
subjectRef = h.DetectionId, // your own immutable id
device = detectorFamiliarId,
dataTimeStamp = h.SeenUtc,
timeZone = "America/New_York",
detectorType = "Acme FR 2.1",
confidence = h.Score,
}).ToArray();
var response = await http.PostAsJsonAsync($"oa/plugins/{oaId}/subject-detections", batch);
response.EnsureSuccessStatusCode();
// Later, when the subject leaves frame:
await http.PostAsJsonAsync($"oa/plugins/{oaId}/subject-detections/close",
new[] { new { subjectRef = hit.DetectionId, lastDetectionUtc = DateTime.UtcNow } });
# Faces carry no images, so batches can be large. A few hundred per call is comfortable.
for chunk in chunked(pending, 200):
resp = requests.post(
f"{BASE}/oa/plugins/{oa_id}/subject-detections",
headers={"Authorization": f"Bearer {token}"},
json=[to_detection(h) for h in chunk],
timeout=60,
)
resp.raise_for_status()
mark_sent(chunk) # only after the server has it
Related endpoints
| Method | Path | What it does |
|---|---|---|
POST | oa/plugins/{id}/subject-detections | Insert detections |
POST | oa/plugins/{id}/subject-detections/close | Close observation segments |
GET | oa/plugins/{id}/subject-detections | Query detections |
GET | oa/plugins/{id}/subject-detections/demographics | Age and gender views |
DELETE | oa/plugins/{id}/subject-detections/{detectId} | Remove one detection |
PUT | oa/plugins/{id}/subject-detectors | Register a detector |
GET | oa/plugins/{id}/subject-detectors | List detectors to map against |
GET | oa/plugins/{id}/subjects/search | Find a subject before attributing a detection |
GET | oa/plugins/{id}/subject-tags | Watch lists a detection can match |
The full list is on the Object Analytics reference page.
Permissions
The service account needs the ObjectAnalytics* keys for the operations it performs — detection
inserts, plus the detector keys if it registers its own detector at install time — and the Object
Analytics plugin instance must be within its
logical group. Creating subjects needs
more than pushing detections does, so if your engine only reports sightings, grant only that. The
FaceRec* keys cover enrolling, merging and splitting, which an ingress account does not need. See
the Permission Keys reference.
What operators see
Pushed detections are ordinary detections. They are found by Face Search, match face tags and raise the alerts configured on them, accumulate as a person's detection history — with the observed duration your close calls produced — and, where the detector is associated with a camera, open the footage of the moment.