API Reference¶
Base URL: https://face-api.xylolabs.com
Interactive docs: /docs (Swagger UI) | /redoc (ReDoc)
Authentication¶
When FACE_API_API_KEY_ENABLED=true, the detect and mask endpoints require an API key via the X-API-Key header.
Keys are managed through the API Key Management page. When API key auth is disabled (default), public endpoints work without authentication.
Rate limiting¶
| Endpoint | Default | Config |
|---|---|---|
POST /api/v1/detect |
60/minute | FACE_API_RATE_LIMIT_DETECT |
POST /api/v1/mask |
30/minute | FACE_API_RATE_LIMIT_MASK |
Per-IP. Returns 429 with Retry-After header when exceeded.
GET /health¶
Health check. No auth needed.
{
"status": "ok",
"model": "scrfd_10g",
"model_loaded": true,
"input_size": [640, 640],
"supports_landmarks": true,
"storage_enabled": false
}
POST /api/v1/detect¶
Detect faces in an image. Returns bounding boxes, 5-point landmarks, and confidence scores.
Content-Type: multipart/form-data
| Parameter | Type | Default | Range | Description |
|---|---|---|---|---|
image |
file | required | — | JPEG, PNG, WebP, BMP, or TIFF |
confidence |
float | 0.5 | 0.01–1.0 | Minimum confidence threshold |
nms_threshold |
float | 0.4 | 0.01–1.0 | NMS IoU threshold |
max_faces |
int | 0 | ≥ 0 | Max faces to return (0 = all) |
Response¶
{
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"image_id": null,
"job_id": null,
"image": {
"width": 1920,
"height": 1080,
"channels": 3,
"size_bytes": 245760
},
"detections": [
{
"index": 0,
"bbox": { "x1": 100.5, "y1": 200.3, "x2": 300.1, "y2": 400.7 },
"confidence": 0.9876,
"landmarks": {
"left_eye": { "x": 150.2, "y": 250.1 },
"right_eye": { "x": 250.3, "y": 248.9 },
"nose": { "x": 200.1, "y": 310.5 },
"left_mouth": { "x": 160.7, "y": 360.2 },
"right_mouth": { "x": 240.5, "y": 358.8 }
},
"size": { "width": 199.6, "height": 200.4 },
"area": 39999.84,
"center": { "x": 200.3, "y": 300.5 },
"relative": {
"x_center": 0.1043, "y_center": 0.2782,
"width": 0.104, "height": 0.1855
}
}
],
"summary": {
"face_count": 1,
"avg_confidence": 0.9876,
"min_confidence": 0.9876,
"max_confidence": 0.9876,
"total_face_area": 39999.84,
"face_area_ratio": 0.019
},
"processing": {
"model": "scrfd_10g",
"input_size": [640, 640],
"confidence_threshold": 0.5,
"nms_threshold": 0.4,
"inference_ms": 12.34,
"total_ms": 18.56
},
"profiling": {
"decode_ms": 3.21,
"preprocess_ms": 1.05,
"blob_ms": 0.82,
"inference_ms": 12.34,
"postprocess_ms": 0.45,
"nms_ms": 0.12,
"total_ms": 18.56,
"det_scale": 0.333333,
"input_resolution": "1920x1080",
"det_resolution": "640x640",
"input_bytes": 245760,
"input_pixels": 2073600,
"model_name": "scrfd_10g",
"model_size_mb": 16.1
}
}
Examples¶
import requests
response = requests.post(
"https://face-api.xylolabs.com/api/v1/detect",
headers={"X-API-Key": "xyl_your_api_key_here"},
files={"image": open("photo.jpg", "rb")},
params={"confidence": 0.6, "max_faces": 5},
)
data = response.json()
print(f"Found {data['summary']['face_count']} face(s)")
for face in data["detections"]:
bbox = face["bbox"]
print(f" Face #{face['index']}: {face['confidence']:.1%} "
f"at ({bbox['x1']:.0f},{bbox['y1']:.0f})-({bbox['x2']:.0f},{bbox['y2']:.0f})")
const formData = new FormData();
formData.append("image", fileInput.files[0]);
const response = await fetch(
"https://face-api.xylolabs.com/api/v1/detect?confidence=0.6",
{
method: "POST",
headers: { "X-API-Key": "xyl_your_api_key_here" },
body: formData,
}
);
const data = await response.json();
console.log(`Found ${data.summary.face_count} face(s)`);
POST /api/v1/mask¶
Detect and mask faces. Returns the processed image as binary.
Content-Type: multipart/form-data
| Parameter | Type | Default | Range | Description |
|---|---|---|---|---|
image |
file | required | — | JPEG, PNG, WebP, BMP, or TIFF |
confidence |
float | 0.5 | 0.01–1.0 | Minimum confidence |
nms_threshold |
float | 0.4 | 0.01–1.0 | NMS IoU threshold |
max_faces |
int | 0 | ≥ 0 | Max faces to mask (0 = all) |
method |
string | gaussian |
see below | Masking method |
strength |
int | 199 | 1–999 | Blur kernel / block size |
padding |
float | 0.15 | 0.0–2.0 | Expand bbox before masking |
format |
string | jpeg |
jpeg, png, webp |
Output format |
quality |
int | 95 | 1–100 | Compression quality |
Masking methods¶
| Method | What it does |
|---|---|
gaussian |
Gaussian blur. strength = kernel size. |
pixelate |
Pixelation. strength = block size. |
solid |
Black rectangle over face. |
elliptical |
Gaussian blur with elliptical mask. |
Response headers¶
The response body is the raw image. Metadata is in headers:
| Header | Description |
|---|---|
X-Request-Id |
Request UUID |
X-Face-Count |
Faces masked |
X-Inference-Ms |
Model inference time |
X-Total-Ms |
Total processing time |
X-Model |
Model name |
X-Image-Width / X-Image-Height |
Original dimensions |
X-Blur-Method |
Method used |
Examples¶
import requests
response = requests.post(
"https://face-api.xylolabs.com/api/v1/mask",
headers={"X-API-Key": "xyl_your_api_key_here"},
files={"image": open("photo.jpg", "rb")},
params={"method": "gaussian", "strength": 99, "quality": 90},
)
with open("masked.jpg", "wb") as f:
f.write(response.content)
print(f"Masked {response.headers['X-Face-Count']} face(s) "
f"in {response.headers['X-Total-Ms']}ms")
const formData = new FormData();
formData.append("image", fileInput.files[0]);
const response = await fetch(
"https://face-api.xylolabs.com/api/v1/mask?method=pixelate&strength=15",
{
method: "POST",
headers: { "X-API-Key": "xyl_your_api_key_here" },
body: formData,
}
);
const blob = await response.blob();
document.getElementById("result").src = URL.createObjectURL(blob);
import requests
from io import BytesIO
from PIL import Image
image_bytes = download_from_somewhere()
response = requests.post(
"https://face-api.xylolabs.com/api/v1/mask",
headers={"X-API-Key": "xyl_your_api_key_here"},
files={"image": ("image.jpg", image_bytes, "image/jpeg")},
params={"method": "elliptical", "padding": 0.3},
)
masked = Image.open(BytesIO(response.content))
Recipes¶
Batch process a folder¶
import requests
from pathlib import Path
API = "https://face-api.xylolabs.com"
KEY = "xyl_your_api_key_here"
for img in Path("photos/").glob("*.jpg"):
r = requests.post(
f"{API}/api/v1/mask",
headers={"X-API-Key": KEY},
files={"image": img.open("rb")},
params={"method": "pixelate", "strength": 15},
)
Path(f"masked/{img.name}").write_bytes(r.content)
print(f"{img.name}: {r.headers['X-Face-Count']} faces")
Count faces (one-liner)¶
curl -s -X POST https://face-api.xylolabs.com/api/v1/detect \
-H "X-API-Key: xyl_your_key" \
-F "image=@group.jpg" | jq '.summary.face_count'
Draw bounding boxes¶
import requests
from PIL import Image, ImageDraw
r = requests.post(
"https://face-api.xylolabs.com/api/v1/detect",
headers={"X-API-Key": "xyl_your_key"},
files={"image": open("photo.jpg", "rb")},
)
img = Image.open("photo.jpg")
draw = ImageDraw.Draw(img)
for face in r.json()["detections"]:
b = face["bbox"]
draw.rectangle([b["x1"], b["y1"], b["x2"], b["y2"]], outline="red", width=3)
draw.text((b["x1"], b["y1"] - 15), f'{face["confidence"]:.0%}', fill="red")
img.save("annotated.jpg")
Pipe to S3¶
curl -s -X POST https://face-api.xylolabs.com/api/v1/mask \
-H "X-API-Key: xyl_your_key" \
-F "image=@photo.jpg" \
-F "format=png" | aws s3 cp - s3://my-bucket/masked/photo.png
Error handling¶
import requests
r = requests.post(
"https://face-api.xylolabs.com/api/v1/detect",
headers={"X-API-Key": "xyl_your_key"},
files={"image": open("photo.jpg", "rb")},
)
if r.status_code == 200:
print(f"{r.json()['summary']['face_count']} faces")
elif r.status_code == 413:
print("Image too large")
elif r.status_code == 429:
print(f"Rate limited, retry in {r.headers.get('Retry-After', '60')}s")
elif r.status_code == 401:
print("Bad API key")
else:
print(f"Error {r.status_code}: {r.json().get('detail')}")
Node.js¶
import fs from "fs";
const form = new FormData();
form.append("image", new Blob([fs.readFileSync("photo.jpg")]), "photo.jpg");
const r = await fetch("https://face-api.xylolabs.com/api/v1/detect?confidence=0.7", {
method: "POST",
headers: { "X-API-Key": "xyl_your_key" },
body: form,
});
const { summary, detections } = await r.json();
console.log(`${summary.face_count} faces`);
detections.forEach(d => {
const { x1, y1, x2, y2 } = d.bbox;
console.log(` #${d.index}: (${x1|0},${y1|0})-(${x2|0},${y2|0}) ${(d.confidence*100).toFixed(1)}%`);
});
Error codes¶
| Status | Meaning |
|---|---|
400 |
Bad image format, too small (< 10x10), or decode failure |
401 |
Missing or invalid API key |
403 |
API key revoked |
413 |
Image exceeds 20 MB or 30M pixels |
429 |
Rate limit exceeded |
503 |
Server busy, retry in 1 second |
Limits¶
| What | Default | Config |
|---|---|---|
| Max file size | 20 MB | FACE_API_MAX_IMAGE_BYTES |
| Max pixels | 30,000,000 | FACE_API_MAX_IMAGE_PIXELS |
| Min size | 10 x 10 px | — |
| Formats | JPEG, PNG, WebP, BMP, TIFF | — |
| Concurrent requests | 10 | FACE_API_MAX_CONCURRENT_REQUESTS |