Relay a Face Detection
relay-ingest/face accepts one face detection from another site or system and records it on the receiving Identities
server as a native detection. The body is the same WebHookFaceRecPayload that an
Events Bridge face WebHook sends, so an Events Bridge can post
here with no code. Any other client can build the same JSON.
Read the Relay Ingest Overview first for how the receiver identifies people, merges details and drops duplicates.
Quick Reference
| Item | Value |
|---|---|
| Request | POST {baseUrl}/api/v1/relay-ingest/face, Content-Type: application/json |
| Auth | HTTP Basic: username = Key ID, password = secret (from the receiver's admin) |
| Required | payload.detectionInfo.dateTimeUtc and payload.detectionImage.image |
| Makes it "known" | Any of payload.personInfo names, tags, fields or notes |
| Success | 200 with an outcome. Every 200 is final. |
| Retry | Only 503 (honor Retry-After) and network failures, with the same detectId |
| Never retry | 200 and 403 |
API Endpoint
- HTTP Method:
POST - Endpoint:
/api/v1/relay-ingest/face - Base URL (Cloud):
https://customername.econnectcloud.com/identities - Base URL (On-prem):
https://<server-host-or-ip>:5009 - Content-Type:
application/json - Available from: Identities 0.23.0
The receiver's admin page shows the complete postback address when it creates the source's credentials.
Authentication
Relay Ingest does not use the JWT from Authentication. Each sender has its own relay source credentials, created by an administrator of the receiving server under Setup → Integration → Relay Ingest:
| Credential | Format | Sent as |
|---|---|---|
| Key ID | 8 characters, A–Z and 2–7, for example ABCD2345. Public; matched case-insensitively. | HTTP Basic username |
| Secret | 43 characters, base64url. Shown once; the receiver keeps only a SHA-256 hash. | HTTP Basic password |
Authorization: Basic <base64 of "KeyId:secret">
The credentials identify the sender. Nothing in the body is trusted for identity: sourceName, serverContext and
externalId are informational only. If the secret is lost, the administrator rotates the key, which replaces both
the Key ID and the secret and stops the old pair immediately.
A request is accepted only when the credentials are valid, the source is enabled, and the Relay Ingest module is
enabled on the receiver. Every failure of these checks returns 403. This endpoint never returns 401.
Minimum Permission
None. The source credentials replace user permissions on this endpoint. Creating sources and keys requires the Relay Ingest Administrator permission in the receiver's web app.
Request Body
The envelope is an Events Bridge WebHookFaceRecPayload. Property names are matched case-insensitively, so both
camelCase and PascalCase work. Images are base64 strings. Only payload is used. The envelope properties
publishType, sourceName, externalId, fields and serverContext are accepted and ignored.
Requests are limited to 30 MB by default. A detection with two JPEG images is normally well under 1 MB.
Required Fields
| Field | Type | Description |
|---|---|---|
payload.detectionInfo.dateTimeUtc | string (ISO-8601) | When the face was captured. Kept exactly. A value without a Z or an offset is read as UTC. |
payload.detectionImage.image | string (base64) | The snapshot: a full frame or a generous crop (JPEG) that contains the face. Do not send a tightly cropped, aligned face. When the image holds several faces, the largest is the relayed one, and Face Recognition records the others as ordinary detections. |
Detection Fields
| Field | Type | Description |
|---|---|---|
payload.detectionInfo.detectId | string | Your unique ID for this detection. Send the same value on every retry. A detectId received in the last hour is a Duplicate. |
payload.detectionInfo.detectorName | string | The camera name. Recorded as <camera prefix><detectorName>, for example Tahoe - Lobby. Defaults to Relay when empty. Also used by the duplicate rule. |
payload.detectionInfo.timeZone | string | IANA time zone. Accepted, but Face Recognition currently records the receiver's zone instead. |
payload.detectionInfo.confidence | number | Accepted and ignored. |
payload.defaultPhoto.image | string (base64) | Optional reference photo of the person. It is searched first when identifying the person, and enrolled when a new profile is created. Send a clear, frontal photo for known people when you have one. |
Person Fields
Any name, tag, field or note makes the detection known. With none of them it is unknown, and the receiver ignores
it (IgnoredUnknown) unless the source is set to record unknown faces.
| Field | Type | Description |
|---|---|---|
payload.personInfo.faceId | string | Your stable ID for this person's face. The receiver links it to its own face, so later detections skip the face search. It must be unique across every sender to the receiver: use a GUID. Used for known detections only. |
payload.personInfo.firstName | string | First name. |
payload.personInfo.middleName | string | Middle name. Dropped if it equals lastName. |
payload.personInfo.lastName | string | Last name. |
payload.personInfo.tags[] | object[] | { "tagName": string, "expires": ISO-8601 or null }. Tags are matched by name and created on the receiver when missing. expires is copied. |
payload.personInfo.fields[] | object[] | { "fieldName": string, "stringValue" | "boolValue" | "numberValue" | "dateValue" }. The first non-empty value in that order is used. Empty values are skipped. See Custom Fields. |
payload.personInfo.notes[] | object[] | { "noteTitle": string, "noteBody": string }. Notes titled eConnect Sync Notes or eConnect Shared Sync Notes are never relayed. |
How these values change a person the receiver already has depends on the source's merge rules. A person the relay creates always receives all of them.
Ignored Fields
payload.detectionHistory, payload.resentEvent and payload.messageId are accepted and ignored. Deduplication uses
detectId, not messageId.
Custom Fields
fieldName is matched to one of the receiver's custom fields, in this order:
- the source's field map, maintained by the receiver's administrator;
- a receiver field with the same ID, such as the built-in
MOBILE_PHONE_NUMBER,PHONE_NUMBER,EMAIL,EMPLOYEE_IDorLOYALTY_CARD; - a receiver field whose friendly name equals
fieldName, ignoring case, for example"Member Number".
A field that matches nothing is skipped, and the receiver never creates fields. The receiver's admin page lists unmatched field names with a sample value so that they can be mapped.
If you control the sender, use the receiver's built-in field IDs or its friendly field names as fieldName. They match
without any field map.
Minimal Example
This detection is unknown (no person details). It is recorded only if the source records unknown faces. Otherwise
the response is IgnoredUnknown.
{
"payload": {
"detectionInfo": {
"dateTimeUtc": "2026-10-02T17:04:22.918Z",
"detectId": "3b8e2f61-7c4a-4d19-a2b5-90e6f1c7d842",
"detectorName": "Lobby"
},
"detectionImage": { "image": "/9j/4AAQSkZJRgABAQEASABIAAD..." }
}
}
Full Example
A known detection with every field Relay Ingest uses:
{
"publishType": "WebHookFaceRecPayload",
"sourceName": "Tahoe Identities",
"payload": {
"personInfo": {
"faceId": "6f1c2a9e-4b7d-4f3a-9c2e-1d5b8a7f0e34",
"firstName": "Jane",
"middleName": "",
"lastName": "Smith",
"tags": [
{ "tagName": "VIP", "expires": null }
],
"fields": [
{ "fieldName": "EMAIL", "stringValue": "jane.smith@example.com" },
{ "fieldName": "Member Number", "stringValue": "100245" }
],
"notes": [
{ "noteTitle": "Host", "noteBody": "Prefers the north entrance." }
]
},
"detectionInfo": {
"dateTimeUtc": "2026-10-02T17:04:22.918Z",
"timeZone": "America/Los_Angeles",
"confidence": 0.97,
"detectId": "3b8e2f61-7c4a-4d19-a2b5-90e6f1c7d842",
"detectorName": "Lobby"
},
"detectionImage": { "image": "/9j/4AAQSkZJRgABAQEASABIAAD..." },
"defaultPhoto": { "image": "/9j/4AAQSkZJRgABAQEASABIAAD..." }
}
}
Raw Request
POST https://customername.econnectcloud.com/identities/api/v1/relay-ingest/face
Authorization: Basic QUJDRDIzNDU6eW91cl80M19jaGFyYWN0ZXJfc2VjcmV0
Content-Type: application/json
{ "payload": { ... } }
The header above encodes the placeholder pair ABCD2345:your_43_character_secret.
Code Examples
- Curl
- PowerShell
- C#
API_URL="https://customername.econnectcloud.com/identities"
KEY_ID="ABCD2345" # Basic Auth username (Key ID)
SECRET="your_43_character_secret" # Basic Auth password (secret)
# A full snapshot (not a tight crop) that contains the face.
IMAGE_B64=$(base64 -w 0 snapshot.jpg)
# Write the body to a file: base64 images are too large for a command-line argument.
cat > relay-body.json <<EOF
{
"payload": {
"personInfo": {
"faceId": "6f1c2a9e-4b7d-4f3a-9c2e-1d5b8a7f0e34",
"firstName": "Jane",
"lastName": "Smith",
"tags": [ { "tagName": "VIP" } ]
},
"detectionInfo": {
"dateTimeUtc": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"timeZone": "America/Los_Angeles",
"detectId": "$(uuidgen)",
"detectorName": "Lobby"
},
"detectionImage": { "image": "$IMAGE_B64" }
}
}
EOF
# 200 = processed (read "outcome"), 403 = fix credentials or configuration, 503 = retry later.
curl -s -X POST "$API_URL/api/v1/relay-ingest/face" \
-u "$KEY_ID:$SECRET" \
-H "Content-Type: application/json" \
--data-binary @relay-body.json \
-w "\nHTTP %{http_code}\n"
# PowerShell 7+
$API_URL = "https://customername.econnectcloud.com/identities"
$KEY_ID = "ABCD2345" # Basic Auth username (Key ID)
$SECRET = "your_43_character_secret" # Basic Auth password (secret)
$IMAGE_FILE = "snapshot.jpg" # a full snapshot that contains the face
$basic = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($KEY_ID + ":" + $SECRET))
$body = @{
payload = @{
personInfo = @{
faceId = "6f1c2a9e-4b7d-4f3a-9c2e-1d5b8a7f0e34" # stable per person, unique across senders
firstName = "Jane"
lastName = "Smith"
tags = @(@{ tagName = "VIP" })
}
detectionInfo = @{
dateTimeUtc = (Get-Date).ToUniversalTime().ToString("o")
timeZone = "America/Los_Angeles"
detectId = [guid]::NewGuid().ToString() # reuse the same value on a retry
detectorName = "Lobby"
}
detectionImage = @{
image = [Convert]::ToBase64String([IO.File]::ReadAllBytes((Resolve-Path $IMAGE_FILE)))
}
}
} | ConvertTo-Json -Depth 10
$response = Invoke-RestMethod -Uri "$API_URL/api/v1/relay-ingest/face" -Method Post `
-Headers @{ Authorization = "Basic $basic" } `
-Body $body -ContentType "application/json" `
-SkipHttpErrorCheck -StatusCodeVariable status
# 200 = processed (read outcome), 403 = fix credentials or configuration, 503 = retry later.
"HTTP $status outcome=$($response.outcome) reason=$($response.reason)"
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
const string ApiUrl = "https://customername.econnectcloud.com/identities";
const string KeyId = "ABCD2345"; // Basic Auth username (Key ID)
const string Secret = "your_43_character_secret"; // Basic Auth password (secret)
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(60) };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{KeyId}:{Secret}")));
// Built once, so every retry sends the same detectId and capture time.
var body = new
{
payload = new
{
personInfo = new
{
faceId = "6f1c2a9e-4b7d-4f3a-9c2e-1d5b8a7f0e34", // stable per person, unique across senders
firstName = "Jane",
lastName = "Smith",
tags = new[] { new { tagName = "VIP" } },
fields = new[] { new { fieldName = "EMAIL", stringValue = "jane.smith@example.com" } }
},
detectionInfo = new
{
dateTimeUtc = DateTime.UtcNow, // capture time, kept exactly
timeZone = "America/Los_Angeles",
detectId = Guid.NewGuid().ToString(),
detectorName = "Lobby"
},
// byte[] is serialized as base64.
detectionImage = new { image = await File.ReadAllBytesAsync("snapshot.jpg") },
defaultPhoto = new { image = await File.ReadAllBytesAsync("reference.jpg") }
}
};
for (var attempt = 1; ; attempt++)
{
using var response = await http.PostAsJsonAsync($"{ApiUrl}/api/v1/relay-ingest/face", body);
if (response.StatusCode == HttpStatusCode.OK)
{
// Every 200 is final: Recorded, Duplicate, IgnoredUnknown, RejectedInvalid or RejectedNoFace.
var result = await response.Content.ReadFromJsonAsync<RelayIngestResponse>();
Console.WriteLine($"{result!.Outcome} {result.Reason} localFaceId={result.LocalFaceId}");
break;
}
if (response.StatusCode == HttpStatusCode.ServiceUnavailable && attempt < 10)
{
// Busy, timed out or temporarily unavailable: wait, then send the same body again.
await Task.Delay(TimeSpan.FromSeconds(Math.Min(60, 5 * attempt)));
continue;
}
// 403: bad credentials, a disabled source, or Relay Ingest switched off. Fix it; do not retry.
Console.WriteLine($"Not accepted: {(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
break;
}
record RelayIngestResponse(string Outcome, string? Reason, string? LocalFaceId, string? DetectId);
Replace the placeholders (base URL, Key ID, secret and image files) with your own values.
Response
Every processed detection returns 200 OK with this body:
{
"outcome": "Recorded",
"reason": null,
"localFaceId": "8d2f4c1a-93b7-4e05-b6a1-2c7e9f0d3a58",
"detectId": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}
| Field | Type | Description |
|---|---|---|
outcome | string | What happened. See Outcomes. |
reason | string or null | A machine-readable detail. Several reasons are joined with ; . |
localFaceId | string or null | The receiver's face the detection was recorded on (Recorded), or the expected face (Duplicate, when one was found). |
detectId | string or null | The receiver's own ID for the recorded detection. Set for Recorded only. It is not your detectId. |
Outcomes
Every outcome is final. Do not retry any 200.
outcome | reason values | Meaning |
|---|---|---|
Recorded | null, MatchedAnotherFace, MergeErrors:N | Stored, shown on the Live Monitor and published. MatchedAnotherFace: Face Recognition filed it under a different face than the expected person's. MergeErrors:N: N details could not be applied to the person. The detection is still recorded. |
Duplicate | AlreadyRecorded | Already recorded: a retry, a second route or a loop. |
IgnoredUnknown | UnknownFace | No person details, and the source ignores unknown faces. |
RejectedInvalid | MissingPayload, MissingCaptureTime, MissingDetectionImage, MalformedJson | The body is incomplete or not JSON. Fix the sender: the same body never succeeds. |
RejectedNoFace | NoFaceInPhoto, NoFaceInDetectionImage | No face was found in the photo used to create the person, or in the snapshot. |
Response Codes
| Code | Body | Meaning | Sender action |
|---|---|---|---|
200 OK | The response above | Processed. Read outcome. | Done. Never retry. |
403 Forbidden | { "reason": "Forbidden" } | The Authorization header is missing or malformed, the Key ID is unknown, the secret is wrong, or the source is disabled. | Do not retry. Fix the credentials or ask the receiver's admin to enable the source. |
403 Forbidden | { "reason": "RelayIngestDisabled" } | The Relay Ingest module is switched off on the receiver. | Do not retry. Ask the receiver's admin. |
503 Service Unavailable | { "reason": "Busy" } | The receiver is already processing its maximum number of detections. | Retry after the Retry-After delay (5 seconds). |
503 Service Unavailable | { "reason": "Timeout" } or { "reason": "Unavailable" } | Processing ran out of time, or Face Recognition or the database failed. | Retry with backoff, using the same detectId. |
503 Service Unavailable | { "reason": "RecordingDisabled" } | Detection recording is switched off on the receiver. | Keep the detection and retry later. |
Events Bridge follows these rules on its own: it drops a 403 without retrying, and retries other failures according to
the WebHook's retry settings.
Sending Reliably
- Retry only
503and network failures, with exponential backoff, and honorRetry-After. - Keep
detectIdanddateTimeUtcidentical on every attempt. If an earlier attempt succeeded but its response was lost, the retry comes back asDuplicateinstead of creating a second detection. - Use a client timeout of at least 60 seconds. The receiver allows itself 30 seconds per detection.
- Send one at a time, or only a few in parallel. The receiver processes 4 detections at once by default.
- Use HTTPS with a certificate the client validates. HTTP Basic credentials are only as safe as the connection that carries them.
Related
- Relay Ingest Overview: resolution, merge rules, duplicates and server settings.
- Face WebHook: every field of the
WebHookFaceRecPayload. - Relay Ingest user guide: creating sources and keys.