Skip to content

Face Recognition & People Counting

Enroll and manage known people, query face-match events, tune the matching threshold, and pull camera people-counting analytics through the Rhombus API.

On this page

Overview

This guide covers two distinct camera analytics capabilities that are easy to confuse:

  • Face Recognition (identity) — enroll known people from face images, then receive face-match events when a camera detects a matching face. Each event carries a confidence score and the best-matching person. Use this to recognize who appears on camera.
  • People counting (headcount) — aggregate, non-identifying camera analytics that count how many people cross a camera's field of view over time. These come from the Report Webservice as count time series and occupancy counts. No identity is involved.

Through the API you can:

  • Enroll and manage people by creating person records and uploading face images
  • Query face-match events with rich filters and pagination
  • Tune the matching threshold and correct misidentified events
  • Pull people-count analytics as time series or the most recent counts per camera
Warning

Biometric privacy. Face recognition processes biometric data. Many jurisdictions regulate the collection, storage, and use of biometric identifiers, and some require notice or consent. Review the biometric-privacy laws that apply to your locations before enrolling faces, and confirm your organization's policies. This guide describes only the API surface — it is not legal advice.

Prerequisites

All requests are POST to https://api2.rhombussystems.com and authenticate with the x-auth-scheme and x-auth-apikey headers. Timestamps are epoch milliseconds unless noted, and match confidence is a float between 0 and 1.

Enroll and manage known people

A person is a named identity record. A matchmaker is an enrolled face template attached to a person; the recognition engine compares detected faces against your matchmakers.

Create a person

Create a named person record to enroll against.

import requests

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

response = requests.post(
    "https://api2.rhombussystems.com/api/faceRecognition/person/createPerson",
    headers=headers,
    json={"name": "Jordan Rivera"}
)

person = response.json().get("person", {})
print(f"Created person {person.get('name')} -> {person.get('uuid')}")
namestringbodyrequired

Display name for the person.

The response returns a person object:

uuidstring
Unique identifier for the person.
namestring
Display name.
emailstring
Optional email associated with the person.
orgUuidstring
Organization the person belongs to.
createdOnstring
When the person record was created.
updatedOnstring
When the person record was last updated.

Upload a face image

Enroll a face by uploading a .jpg or .png image (maximum 5 MB) as multipart/form-data. The file must be sent under the form field name file. Two optional query parameters control the transaction:

  • transaction — a transaction ID you supply to track and later poll the upload. If omitted, the server generates one and returns it.
  • createPersonIfNotFound — set to true to have the system create a new person automatically when the uploaded face does not match an existing one.
import requests

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY"
    # Do NOT set Content-Type manually; requests sets the multipart boundary
}

with open("jordan_rivera.jpg", "rb") as f:
    files = {"file": ("jordan_rivera.jpg", f, "image/jpeg")}
    response = requests.post(
        "https://api2.rhombussystems.com/api/faceRecognition/matchmaker/uploadFaceMatchmakers",
        headers=headers,
        params={"transaction": "enroll-jordan-001", "createPersonIfNotFound": "true"},
        files=files
    )

data = response.json()
print(f"Transaction: {data.get('transactionId')}")
for result in data.get("fileUploadResults", []):
    print(f"  {result.get('fileName')}: success={result.get('success')} {result.get('message', '')}")
Warning

Upload one image per request. Send exactly one file part, keep it under 5 MB, and use .jpg or .png. Let your HTTP client set the Content-Type: multipart/form-data boundary — do not set it by hand.

The response returns:

transactionIdstring
The transaction ID for this upload (echoed back, or generated if you omitted it). Use it to poll processing status.
fileUploadResultsarray
Per-file upload results, each with fileName, success (boolean), and message.

Poll the upload transaction

Face images process asynchronously. Poll the transaction to learn the resulting faceId and the personUuid the face was enrolled or linked to.

import requests
import time

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

for _ in range(10):
    response = requests.post(
        "https://api2.rhombussystems.com/api/faceRecognition/matchmaker/findFaceUploadMetadataByTransaction",
        headers=headers,
        json={"transactionId": "enroll-jordan-001"}
    )
    metadata = response.json().get("faceUploadMetadata", [])
    if metadata and all(m.get("success") is not None for m in metadata):
        for m in metadata:
            print(f"faceId={m.get('faceId')} personUuid={m.get('personUuid')} "
                  f"success={m.get('success')} {m.get('errorMsg', '')}")
        break
    time.sleep(2)

Each entry in faceUploadMetadata includes:

FieldTypeDescription
transactionIdstringThe transaction this upload belongs to
faceIdstringIdentifier of the enrolled face matchmaker (once processed)
personUuidstringPerson the face was enrolled or linked to
successbooleanWhether processing succeeded
errorMsgstringError detail when success is false
createdAtMillisintegerUpload timestamp in epoch milliseconds
origS3KeystringStorage key for the original uploaded image
Tip

Prefer to enroll a person from a face the camera already captured? Use createFaceMatchmakerFromSighting with the faceEventUuid of an existing face event and the target personUuid to turn that sighting into an enrolled template — no upload required.

Label a person

Attach labels (for example, employee or visitor) to organize people and filter face events later.

import requests

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

response = requests.post(
    "https://api2.rhombussystems.com/api/faceRecognition/person/addPersonLabel",
    headers=headers,
    json={"personUuid": "YOUR_PERSON_UUID", "label": "employee"}
)
print(response.json())

Use removePersonLabel with the same body to detach a label, and findPersonLabelsByOrg (empty body {}) to retrieve a labelsByPerson map of every person's labels.

List people and matchmakers

List every enrolled person with findPeopleByOrg, and list enrolled face templates with findFaceMatchmakersByOrg (or findFaceMatchmakersByPerson for one person).

import requests

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

people = requests.post(
    "https://api2.rhombussystems.com/api/faceRecognition/person/findPeopleByOrg",
    headers=headers, json={}
).json().get("people", [])

for person in people:
    print(f"{person.get('name')} ({person.get('uuid')})")

matchmakers = requests.post(
    "https://api2.rhombussystems.com/api/faceRecognition/matchmaker/findFaceMatchmakersByOrg",
    headers=headers, json={}
).json().get("faceMatchmakers", [])

for mm in matchmakers:
    print(f"faceId={mm.get('id')} personUuid={mm.get('personUuid')} uploaded={mm.get('uploaded')}")

Each face matchmaker includes id (the face ID), personUuid, orgUuid, uploaded (boolean), and createdOn.

Update or remove people and faces

EndpointBodyPurpose
/api/faceRecognition/person/getPerson{ "personUuid": "..." }Fetch one person
/api/faceRecognition/person/updatePerson{ "personSelectiveUpdate": { "uuid": "...", "name": "..." } }Update name/email (selective)
/api/faceRecognition/person/deletePerson{ "personUuid": "..." }Delete a person
/api/faceRecognition/matchmaker/getFaceMatchmaker{ "faceId": "..." }Fetch one enrolled face
/api/faceRecognition/matchmaker/deleteFaceMatchmaker{ "faceId": "..." }Remove an enrolled face template

Query face-match events

When a camera detects a face, it generates a face event. Retrieve events with findFaceEventsByOrg, which takes a searchFilter and a pageRequest.

import requests
import time

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

now_ms = int(time.time() * 1000)
one_day_ago_ms = now_ms - (24 * 60 * 60 * 1000)

response = requests.post(
    "https://api2.rhombussystems.com/api/faceRecognition/faceEvent/findFaceEventsByOrg",
    headers=headers,
    json={
        "searchFilter": {
            "hasName": True,
            "timestampFilter": {
                "rangeStart": str(one_day_ago_ms),
                "rangeEnd": str(now_ms)
            }
        },
        "pageRequest": {"maxPageSize": 50}
    }
)

data = response.json()
for event in data.get("faceEvents", []):
    match = event.get("selectedPersonMatch") or {}
    print(f"{event.get('faceName')} on {event.get('deviceUuid')} "
          f"confidence={match.get('confidence')} at {event.get('eventTimestamp')}")

# Fetch the next page if lastEvaluatedKey is present
next_key = data.get("lastEvaluatedKey")

Search filter

All searchFilter fields are optional. When timestampFilter is omitted, the search defaults to the last 7 days.

personUuidsstring[]body
Filter to a set of people.
faceNamesstring[]body
Filter by exact person names.
faceNameContainsstringbody
Filter by a name substring (minimum 3 characters after trimming). Takes precedence over faceNames if both are set.
labelsstring[]body
Filter by person labels.
deviceUuidsstring[]body
Filter to a set of cameras.
locationUuidsstring[]body
Filter to a set of locations.
hasNamebooleanbody
Filter by the presence (true) or absence (false) of a matched person name.
hasEmbeddingbooleanbody
Filter by the presence or absence of a face embedding.
timestampFilterobjectbody
Time window with rangeStart and rangeEnd, each a string containing an epoch-millisecond value (inclusive). Defaults to the last 7 days.

Page request

maxPageSizeintegerbody
Maximum number of events to return in one page.
lastEvaluatedKeystringbody
Pagination cursor. Pass the lastEvaluatedKey from the previous response to fetch the next page. Absent when there are no more results.

Interpreting a face event

Each event in faceEvents is an object with these fields:

uuidstring
Unique identifier for the face event.
eventTimestampinteger
When the face was detected, in epoch milliseconds.
faceNamestring
Name of the matched person. Equal to the name of selectedPersonMatch when a match was selected.
personUuidstring
UUID of the matched person, if any.
deviceUuidstring
Camera that generated the event.
locationUuidstring
Location of the camera.
detectionConfidencenumber
Confidence (0–1) that the detected image is a face.
embeddingConfidencenumber
Confidence (0–1) associated with the generated face signature.
hasEmbeddingboolean
Whether the event has a face signature.
selectedPersonMatchobject
The chosen person match, with uuid, name, faceId, and confidence (0–1). Null when no match was selected.
topPersonMatchesarray
The best candidate matches for the face, each with uuid, name, faceId, and confidence (0–1). Useful for reviewing near-matches.
imageS3Keystring
Storage key for the event image.
thumbnailS3Keystring
Storage key for the event thumbnail.
Tip

Fetch a single event by its UUID with getFaceEvent (body { "eventUuid": "..." }).

Tune matching and correct events

Read and update the matching threshold

The matching threshold controls how strict face matching is. It is a confidence value between 0 and 1 — a higher value requires a closer match (fewer false positives, more misses); a lower value is more permissive.

import requests

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

# Read current threshold
current = requests.post(
    "https://api2.rhombussystems.com/api/faceRecognition/matchmaker/getFaceMatchingConfig",
    headers=headers, json={}
).json()
print(f"Current threshold: {current.get('faceMatchConfidenceThreshold')}")

# Raise the threshold to reduce false positives
updated = requests.post(
    "https://api2.rhombussystems.com/api/faceRecognition/matchmaker/updateFaceMatchingConfig",
    headers=headers,
    json={"faceMatchConfidenceThreshold": 0.8}
).json()
print(f"New threshold: {updated.get('faceMatchConfidenceThreshold')}")
faceMatchConfidenceThresholdnumberbody
Minimum match confidence (0–1) required to consider a detected face a match.

Correct a misidentified event

Reassign a face event to the correct person (or set a name directly) with updateFaceEvent. This is how you fix false matches.

import requests

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

response = requests.post(
    "https://api2.rhombussystems.com/api/faceRecognition/faceEvent/updateFaceEvent",
    headers=headers,
    json={
        "eventUuid": "YOUR_FACE_EVENT_UUID",
        "personUuid": "CORRECT_PERSON_UUID"
    }
)
print(response.json())
eventUuidstringbodyrequired
The face event to update.
personUuidstringbody
UUID of the correct person to associate with the event.
personNamestringbody
Name to associate with the event. Ignored if personUuid matches one of the event's top person matches.

To remove an event entirely, call deleteFaceEvent with { "eventUuid": "..." }.

People-counting analytics

People counting is aggregate camera analytics — it counts people without identifying them, served by the Report Webservice. Use it for footfall, occupancy, and trend dashboards.

Count time series

getCountReportV2 returns a time series of counts bucketed by interval. For people counting, set types to ["PEOPLE"].

import requests
import time

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

now_ms = int(time.time() * 1000)
week_ago_ms = now_ms - (7 * 24 * 60 * 60 * 1000)

response = requests.post(
    "https://api2.rhombussystems.com/api/report/getCountReportV2",
    headers=headers,
    json={
        "startTimeMs": week_ago_ms,
        "endTimeMs": now_ms,
        "interval": "DAILY",
        "scope": "DEVICE",
        "types": ["PEOPLE"],
        "uuid": "YOUR_CAMERA_UUID",
        "timeZone": "America/Los_Angeles"
    }
)

data = response.json()
for point in data.get("timeSeriesDataPoints", []):
    counts = point.get("eventCountMap", {})
    print(f"{point.get('dateLocal')}: {counts.get('PEOPLE', 0)} people")
startTimeMsintegerbodyrequired
Start of the range in epoch milliseconds.
endTimeMsintegerbodyrequired
End of the range in epoch milliseconds.
intervalstringbodyrequired
Bucket size. One of MINUTELY, QUARTERHOURLY, HOURLY, DAILY, WEEKLY, MONTHLY.
scopestringbodyrequired
Aggregation scope. One of REGION, DEVICE, LOCATION, ORG.
typesstring[]bodyrequired
Report types to include. Use ["PEOPLE"] for people counting.
uuidstringbody
Target UUID for the chosen scope (for example, a camera UUID when scope is DEVICE). Omit for ORG scope.
timeZonestringbody
IANA time zone used to bucket the data (for example, America/Los_Angeles).

Each entry in timeSeriesDataPoints includes:

dateUtcstring
Bucket start in UTC.
dateLocalstring
Bucket start in the requested time zone.
eventCountMapobject
Map of report type to count for the bucket (for example, { "PEOPLE": 42 }).
reportingDevicesMapobject
Map describing which devices reported into the bucket.

Occupancy counts over time

getOccupancyCountsV2 returns a time series of camera-derived occupancy counts for a single camera.

import requests
import time

headers = {
    "x-auth-scheme": "api-token",
    "x-auth-apikey": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

now_ms = int(time.time() * 1000)
day_ago_ms = now_ms - (24 * 60 * 60 * 1000)

response = requests.post(
    "https://api2.rhombussystems.com/api/report/getOccupancyCountsV2",
    headers=headers,
    json={
        "deviceUuid": "YOUR_CAMERA_UUID",
        "startTimeMs": day_ago_ms,
        "endTimeMs": now_ms,
        "interval": "HOURLY"
    }
)

for point in response.json().get("timeSeriesDataPoints", []):
    print(f"{point.get('dateLocal')}: {point.get('eventCountMap')}")
deviceUuidstringbodyrequired
Camera to read occupancy counts from.
startTimeMsintegerbodyrequired
Start of the range in epoch milliseconds.
endTimeMsintegerbodyrequired
End of the range in epoch milliseconds.
intervalstringbodyrequired
Bucket size (MINUTELY, QUARTERHOURLY, HOURLY, DAILY, WEEKLY, MONTHLY).

Each data point adds timestampMs and approximateTimestampMsMap to the standard dateUtc, dateLocal, eventCountMap, and reportingDevicesMap fields.

Reference: report enums

EnumValues
Report typeCROWD, PEOPLE, FACES, MOTION, BANDWIDTH, VEHICLES, LICENSEPLATES, ALERTS, AM_VERIFICATION, DWELL
IntervalMINUTELY, QUARTERHOURLY, HOURLY, DAILY, WEEKLY, MONTHLY
ScopeREGION, DEVICE, LOCATION, ORG

Use cases

  • Watchlist alerting — enroll people of interest, then poll findFaceEventsByOrg filtered by personUuids or labels to flag sightings.
  • Access reconciliation — cross-reference face events with access-control activity to confirm the person who badged in matches who the camera saw.
  • Model quality loop — review topPersonMatches on low-confidence events, correct them with updateFaceEvent, and tune faceMatchConfidenceThreshold to balance false positives against misses.
  • Footfall dashboards — chart getCountReportV2 with types: ["PEOPLE"] for daily or hourly traffic trends per camera or location.
  • Occupancy tiles — chart getOccupancyCountsV2 for a camera to show occupancy through the day.

Troubleshooting

Face endpoints return an error or are unavailable

Face Recognition is a licensed, gated feature. Confirm your organization is entitled and that your API key has face-management permissions. Contact your Rhombus account team if the endpoints are not available.

uploadFaceMatchmakers rejects my file

Send exactly one file per request under the form field name file, keep it under 5 MB, and use .jpg or .png. Let your HTTP client set the multipart Content-Type boundary — do not set Content-Type manually.

findFaceUploadMetadataByTransaction returns nothing yet

Uploads process asynchronously. Poll the transaction until each entry reports a success value. When success is false, read errorMsg for the reason.

No face events come back

If you omit timestampFilter, the search only covers the last 7 days. Widen the window with rangeStart/rangeEnd (epoch-millisecond strings), and loosen filters such as hasName or personUuids. Remember to page with lastEvaluatedKey.

Too many false matches (or too few matches)

Adjust faceMatchConfidenceThreshold with updateFaceMatchingConfig. Raise it (closer to 1) to reduce false positives; lower it to catch more potential matches. Correct individual mistakes with updateFaceEvent.

Next steps