5.9 KiB
Agent contract — UpsertFacePhoto
Status: implemented on both sides. The agent side lives in Access-device-agent
(Devices/HikvisionIsapiClient.SetUserFaceAsync and the UpsertFacePhoto case in
Services/CommandWorker.DispatchAsync). This document remains the reference for the wire
format and the device call.
Not yet verified against a physical terminal — see "Untested" at the end.
Flow
Web ──POST client/customers/{id}/face-photo──▶ Server
│ validates JPEG, queues DeviceCommand
▼
Agent ──GET api/agent/commands ─────────────────▶ receives UpsertFacePhoto
──ISAPI POST────────────────────▶ Hikvision terminal (gym LAN)
──POST api/agent/commands/{id}/result ────▶ Server
│ purges payload, updates profile
▼
SignalR "FacePhotoUpdated" ──▶ browser
Command the agent receives
GET api/agent/commands returns the usual envelope; CommandType is UpsertFacePhoto.
PayloadJson:
{
"bindingId": "8f1c...", // guid, AccessDeviceBinding
"accessProfileId": 6, // long
"employeeNo": "HF6", // the person id already used by UpsertPerson
"name": "Prabodha",
"faceJpegBase64": "/9j/4AAQ...", // bare base64, no data: prefix
"desiredVersion": 4
}
The image is already normalised by the browser and re-checked server-side:
| Property | Guaranteed |
|---|---|
| Format | JPEG (magic bytes verified) |
| Dimensions | 480×640 from the web client; ≤ 1920×1920 enforced |
| Size | ≤ 200 KB enforced, typically 60–150 KB |
So the agent does not need to resize or convert. Decode the base64 and POST it.
Ordering: the person record must exist on the terminal before a face can attach to it.
employeeNo is the same value UpsertPerson uses. If a face command arrives for a person
the terminal doesn't have, run the person upsert first rather than failing.
ISAPI call
Face data is attached to an existing person in a face library. Typical shape for the DS-K1T series (confirm against your firmware — this varies more than Hikvision's docs suggest):
POST /ISAPI/Intelligent/FDLib/FDSetUp?format=json
Content-Type: multipart/form-data; boundary=---boundary
Authorization: Digest ...
---boundary
Content-Disposition: form-data; name="FaceDataRecord"
Content-Type: application/json
{ "faceLibType": "blackFD", "FDID": "1", "FPID": "HF6", "name": "Prabodha" }
---boundary
Content-Disposition: form-data; name="FaceImage"; filename="face.jpg"
Content-Type: image/jpeg
<binary jpeg>
---boundary--
FPIDshould be theemployeeNo, so the face ties to the same person record.- Use Digest auth, as with the other endpoints.
- Replacing a photo is the same call — the device overwrites the FPID's face.
Result the agent posts back
POST api/agent/commands/{id}/result
Success:
{ "success": true, "resultJson": "{\"fpid\":\"HF6\"}" }
The server reads fpid (also accepts FPID, faceId, faceURL) into
AccessProfile.FacePhotoFPID — the handle needed to replace or delete the face later.
Anything longer than 64 chars is truncated.
Failure:
{ "success": false, "resultJson": "{\"detail\":\"No frontal face detected in the image\"}" }
Put a human-readable reason in detail — it is surfaced to gym staff verbatim, so it
should say what to do about it. Real Hikvision rejections worth mapping:
| Device response | Suggested detail |
|---|---|
lowFaceQuality / no face found |
No clear frontal face was detected. Retake with the face centred and well lit. |
| face already bound to another FPID | This face is already enrolled for another member. |
| library full | The terminal's face library is full. |
| image format rejected | The terminal rejected the image format. |
Retry behaviour
MaxAttempts is 3 for face commands (lower than the 5 used for person/state commands):
a face rejected for quality will be rejected identically on every retry, so failing fast
gets the real message in front of staff sooner.
- Retries use the existing exponential backoff.
- On success or final failure the server clears
PayloadJson, so the image is not retained. A retry between those points still has the payload available. - A newer photo supersedes an older queued one — the stale command is marked
Supersededand its payload cleared. The agent should treatSupersededas "stop, discard".
Deleting a face
The agent side is implemented (DeleteUserFaceAsync, plus a DeleteFacePhoto dispatch
case), calling:
PUT /ISAPI/Intelligent/FDLib/FDSearch/Delete?format=json&FDID=1&faceLibType=blackFD
{ "FPID": [ { "value": "HF6" } ] }
The server does not queue this command yet — nothing calls it when a member is removed.
Adding that is a small change: queue DeleteFacePhoto with the stored FacePhotoFPID
wherever a member is deactivated, then clear the face columns on success.
Untested
Everything here compiles and the wire format matches on both sides, but no part of this has run against a physical Hikvision terminal. Expect to verify:
FDID = "1"andfaceLibType = "blackFD"are the right library on your model. Terminals with multiple face libraries may need a different FDID.- The multipart part names (
FaceDataRecord,FaceImage) — these vary between firmware generations more than Hikvision's documentation suggests. - Whether your model accepts 480×640 at ~150 KB, or wants smaller.
The agent logs the full device response for both success and failure
(SetUserFace {EmployeeNo} ... : {Response}), which is the fastest way to see what the
terminal actually objects to on the first real run.