Face APIs
Send an image to Face Detect for geometry only, or to Face Analyze for geometry and visual attributes. The schema on this page is generated from the service’s own models, and the machine-readable version is at /v1/openapi.json if you would rather generate a client.
Read meets_threshold before acting on any value.
A label is always returned, so its presence is not a claim that it is reliable. The flag says whether it cleared the bar fitted for that attribute. The same rule applies to the face itself: false there means the box is the best available candidate and may be background.
meta.release_status is releasedas of 7 August 2026: every artifact behind this endpoint records a named reviewer and a date. That is a statement that the limits were read and accepted, not that there are none — the model cards linked from meta.models carry them, and the section below summarises the ones most likely to affect you.
Authentication
One header, on every request. Create a key in the console; it is shown once and stored only as a peppered hash, so it cannot be shown again.
Authorization: Bearer pd_live_...
Request
POST /v1/face/analyze, with the image as multipart/form-data in an image part. JPEG, PNG or WebP, up to 10 MB and 50 megapixels once decoded.
curl https://pixeldepot.ai/v1/face/analyze \ -H "Authorization: Bearer pd_live_..." \ -F "image=@person.jpg"
image_url is not implemented. Fetching a URL a caller supplies is a server-side request forgery surface and wants an allowlist and timeout policy of its own, so sending JSON returns invalid_request rather than a partial version of it.
Response
200, with X-Inference-Duration-Ms giving the milliseconds we spent calling the model. faces may be empty: rare, and still not evidence the frame contains no person.
{
"request_id": "req_9e3cc8acd17d4087b9b3a593fa4ddd27",
"image": { "width": 1280, "height": 1920 },
"faces": [
{
"id": "face_1",
"region": "bounding_box",
"bounding_box": { "x": 468, "y": 635, "width": 161, "height": 216 },
"confidence": 0.538,
"meets_threshold": true,
"threshold": 0.2,
"attributes": {
"estimated_age": {
"range": "18-64", "estimate": 34, "confidence": 0.957,
"meets_threshold": true, "threshold": 0.4878,
"probabilities": [
{ "label": "18-64", "probability": 0.957 },
{ "label": "0-17", "probability": 0.033 },
{ "label": "65+", "probability": 0.010 }
]
},
"perceived_gender_presentation": {
"label": "Male", "confidence": 0.998,
"meets_threshold": true, "threshold": 0.5,
"probabilities": [
{ "label": "Male", "probability": 0.998 },
{ "label": "Female", "probability": 0.002 }
]
},
"apparent_ancestry": {
"label": "White", "confidence": 0.725,
"meets_threshold": true, "threshold": 0.552,
"probabilities": [
{ "label": "White", "probability": 0.72 },
{ "label": "Latin", "probability": 0.22 },
{ "label": "Asian", "probability": 0.05 },
{ "label": "Black", "probability": 0.01 }
]
},
"has_glasses": {
"label": "no", "confidence": 0.981,
"meets_threshold": true, "threshold": 0.5215,
"probabilities": [
{ "label": "no", "probability": 0.981 },
{ "label": "yes", "probability": 0.019 }
]
}
}
}
],
"meta": {
"models": [
{ "name": "face-detector", "version": "0.2.0", "weight_sha256": "9511f25a..." },
{ "name": "face-attributes", "version": "0.5.0", "weight_sha256": "e5a96e53..." }
],
"disclosures": [],
"release_status": "released"
}
}Face Detect public beta
POST /v1/face/detectuses the same image limits, but returns only boxes clearing the detector’s fitted 0.90-precision operating point. Results are ordered by confidence. If none qualify, the response is still 200 with faces: []. Up to 50 faces are returned; inspect meta.truncated before treating the array as complete.
curl https://pixeldepot.ai/v1/face/detect \ -H "Authorization: Bearer pd_live_..." \ -F "image=@group.jpg"
{
"request_id": "req_33efc95eea754d68823f6b8e3e30af08",
"image": { "width": 1600, "height": 900 },
"faces": [
{
"id": "face_1",
"bounding_box": { "x": 312, "y": 164, "width": 118, "height": 142 },
"confidence": 0.947,
"threshold": 0.3681,
"meets_threshold": true
}
],
"meta": {
"models": [
{ "name": "face-detector", "version": "0.2.0", "weight_sha256": "9511f25a..." }
],
"release_status": "released",
"disclosures": [],
"max_faces": 50,
"truncated": false
}
}Face Detect keeps operational request metadata and aggregate usage only. PixelDepot does not retain the submitted image or response geometry. Face Analyze images and results remain available to the organization for seven days.
Schema
Generated from the contract. A ? marks an optional field.
Face Analyze result.
| field | type | notes |
|---|---|---|
| request_id | string | |
| image | ImageInfo | |
| faces | AnalyzedFace[] | |
| meta | ResponseMeta |
Geometry-only Face Detect result.
| field | type | notes |
|---|---|---|
| request_id | string | |
| image | ImageInfo | |
| faces | DetectedFace[] | |
| meta | DetectMeta |
Dimensions of the submitted image, as decoded.
| field | type | notes |
|---|---|---|
| width | integer | |
| height | integer |
One analyzed face region.
| field | type | notes |
|---|---|---|
| id | string | |
| region | "whole_image" | "bounding_box" | |
| bounding_box ? | BoundingBox | null | |
| confidence ? | number | null | |
| meets_threshold ? | boolean | null | |
| threshold ? | number | null | |
| attributes | FaceAttributes |
One threshold-clearing face returned by Face Detect.
| field | type | notes |
|---|---|---|
| id | string | |
| bounding_box | BoundingBox | |
| confidence | number | |
| threshold | number | |
| meets_threshold ? | boolean |
Face region in **original submitted-image pixels**, ``(0, 0)`` top-left. Not detector-input pixels. The detector sees a letterboxed square tensor and the caller never does, so every coordinate on the wire is in the coordinate system of the image the caller uploaded — the only one they can draw on. Integers, matching PRD §10. A pixel index is an integer, and a float here would imply a precision the detector does not have. The convention is containment: the integer box always encloses the model's float box, so a drawn rectangle never cuts a chin off by a pixel.
| field | type | notes |
|---|---|---|
| x | integer | |
| y | integer | |
| width | integer | |
| height | integer |
Attributes for one face. ``apparent_ancestry`` is absent unless the caller is entitled to it. Absence is the default, not an error (ADR-0007, ADR-0008). ``has_glasses`` is absent on artifacts that predate the head — 0.1.0 through 0.3.0 — rather than defaulted, because a default would be a claim the model never made.
| field | type | notes |
|---|---|---|
| estimated_age | AgeAttribute | |
| perceived_gender_presentation | CategoricalAttribute | |
| apparent_ancestry ? | CategoricalAttribute | null | |
| has_glasses ? | CategoricalAttribute | null | `yes` means lenses in front of the eyes. Sunglasses pushed up onto the head, hanging from a collar or held in the hand are `no`: this describes what is in front of the eyes in this frame, not what the person owns. Absent on artifacts older than face-attributes 0.4.0. |
Age as both a bucket and a representative integer. See AGE_MIDPOINTS.
| field | type | notes |
|---|---|---|
| range | string | |
| estimate | integer | |
| confidence | number | |
| meets_threshold | boolean | |
| threshold | number | |
| probabilities | ClassProbability[] |
A categorical prediction with its full distribution. **`label` always carries a real class.** It is never the string ``"uncertain"``. Owner decision, 2026-08-01: the API must always decide. That is also the model the customer base already knows — Kairos returns a distribution and no "I don't know", and the integrator thresholds it themselves. Refusing to answer and answering badly are still different things, so the distinction moved out of ``label`` and into its own field. ``meets_threshold`` says whether this prediction cleared the validated bar for its head; when it is ``false`` the label is the model's best guess and nothing more. That is strictly more information than the old magic string carried, and it is machine-readable rather than requiring a string comparison.
| field | type | notes |
|---|---|---|
| label | string | |
| confidence | number | |
| meets_threshold | boolean | |
| threshold | number | |
| probabilities | ClassProbability[] |
One class and its calibrated probability.
| field | type | notes |
|---|---|---|
| label | string | |
| probability | number |
Everything needed to audit or reproduce the result.
| field | type | notes |
|---|---|---|
| models | ModelIdentity[] | |
| disclosures | string[] | |
| release_status ? | string |
Audit identity and completeness limits for Face Detect.
| field | type | notes |
|---|---|---|
| models | ModelIdentity[] | |
| disclosures | string[] | |
| release_status ? | string | |
| max_faces | integer | |
| truncated | boolean |
Identity of a model that contributed to this response (PRD §12).
| field | type | notes |
|---|---|---|
| name | string | |
| version | string | |
| weight_sha256 | string | |
| card | string |
| field | type | notes |
|---|---|---|
| error | "feature_unavailable" | "image_too_large" | "inference_timeout" | "inference_unreachable" | "insufficient_quota" | "internal_error" | "invalid_image" | "invalid_key" | "invalid_request" | "missing_authorization" | "rate_limit_exceeded" | |
| message ? | string | |
| request_id | string |
What the attributes are measured on
Age, gender presentation and ancestry come from a model trained on web photography, and their accuracy is published per artifact on the model card linked from meta.models.
has_glassesis different and it is worth knowing how. It is trained on 407 hand-labelled crops from one operator’s photo-booth archives and scored out of fold across 402 sessions: accuracy 0.941, recall 0.958, precision 0.767. That is one venue. Performance elsewhere is unmeasured rather than assumed equal, and 72 positives cannot support a subgroup breakdown. The label means lenses in front of the eyes; sunglasses pushed onto the head are no.
It is also the one attribute computed from a second look at the image. Version 0.4.0 read the same whole-face crop as everything else and missed thin wire, rimless and pale frames: at 224×224 with global average pooling, a two-pixel frame does not survive. 0.5.0 classifies the eye band instead, which took recall from 0.889 to 0.958 with no new labels. The cost is one extra forward pass per face, and it is already in the number X-Inference-Duration-Ms reports.
Errors
The error string is part of the contract; the message is not. Match on the code.
| status | meaning |
|---|---|
| 400 | `invalid_request`: A field is missing, malformed or not recognized. `invalid_image`: The bytes are not a decodable JPEG, PNG or WebP. |
| 401 | `missing_authorization`: No Authorization header was sent. `invalid_key`: The key is unknown, revoked, or not a PixelDepot key. |
| 402 | `insufficient_quota`: The monthly allowance for your plan is spent. |
| 413 | `image_too_large`: Over 10 MB, or over 50 megapixels once decoded. |
| 429 | `rate_limit_exceeded`: Over the per-minute burst for your plan. |
| 500 | `internal_error`: Our fault. The request ID identifies it in our logs. |
| 502 | `inference_unreachable`: The inference service did not answer. Safe to retry. |
| 503 | `feature_unavailable`: No face detector is available on this deployment. |
| 504 | `inference_timeout`: The request could not enter or finish inference in time. |
What these outputs are not
Estimated age, perceived gender presentation and apparent ancestry are visual classifications of an image. None is a statement about a person’s actual age, gender or ethnicity, none is proof of identity, and none may inform employment, credit, housing, insurance or education decisions. apparent_ancestry must not be used for EU processing. See the Terms and the photograph credits.