API Reference¶
Base URL: https://face-api.xylolabs.com
Interactive docs: /docs (Swagger UI) | /redoc (ReDoc)
Authentication¶
Public endpoints require an API key via the X-API-Key header when FACE_API_API_KEY_ENABLED=true (default: true).
Create keys through the API Key Management page or the admin console. Requests without a valid key get 401.
Rate limiting¶
| Endpoint | Limit |
|---|---|
POST /api/v1/detect |
300/minute |
POST /api/v1/mask |
150/minute |
Per-IP. Returns 429 with Retry-After header when exceeded.
GET /health¶
Returns health information. No auth is required. The endpoint returns 200 only when the model and any required DB/storage dependencies are ready; otherwise it returns 503 with a degraded status.
- Request format: none
- Response format: JSON (
HealthResponse)
import requests
response = requests.get("https://face-api.xylolabs.com/health")
print(response.json())
const response = await fetch("https://face-api.xylolabs.com/health");
console.log(await response.json());
using var client = new HttpClient();
var response = await client.GetAsync("https://face-api.xylolabs.com/health");
Console.WriteLine(await response.Content.ReadAsStringAsync());
{
"status": "ok",
"model": "scrfd_10g",
"model_loaded": true,
"input_size": [640, 640],
"supports_landmarks": true,
"database_enabled": false,
"database_ready": null,
"storage_enabled": false,
"storage_ready": null
}
POST /api/v1/detect¶
Detects faces in an image and returns bounding boxes, 5-point landmarks, and confidence scores.
Content-Type: multipart/form-data
- Request format:
multipart/form-data - Response format: JSON (
DetectResponse)
| 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,
"inference_ms": 12.34,
"postprocess_ms": 0.45,
"total_ms": 18.56,
"input_resolution": "1920x1080",
"input_bytes": 245760
}
}
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)`);
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "xyl_your_api_key_here");
using var content = new MultipartFormDataContent();
content.Add(new ByteArrayContent(File.ReadAllBytes("photo.jpg")), "image", "photo.jpg");
var response = await client.PostAsync(
"https://face-api.xylolabs.com/api/v1/detect?confidence=0.6", content);
var json = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json);
var faceCount = doc.RootElement.GetProperty("summary").GetProperty("face_count").GetInt32();
Console.WriteLine($"Found {faceCount} face(s)");
POST /api/v1/mask¶
Detects and masks faces, then returns the processed image as binary.
Content-Type: multipart/form-data
- Request format:
multipart/form-data - Response format: binary image (
image/jpeg,image/png, orimage/webp)
| 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);
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "xyl_your_api_key_here");
using var content = new MultipartFormDataContent();
content.Add(new ByteArrayContent(File.ReadAllBytes("photo.jpg")), "image", "photo.jpg");
var response = await client.PostAsync(
"https://face-api.xylolabs.com/api/v1/mask?method=pixelate&strength=20", content);
var imageBytes = await response.Content.ReadAsByteArrayAsync();
File.WriteAllBytes("masked.jpg", imageBytes);
var faceCount = response.Headers.GetValues("X-Face-Count").First();
Console.WriteLine($"Masked {faceCount} face(s)");
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")
import fs from "fs";
import path from "path";
const API = "https://face-api.xylolabs.com";
const KEY = "xyl_your_api_key_here";
await fs.promises.mkdir("masked", { recursive: true });
for (const name of await fs.promises.readdir("photos")) {
if (!name.endsWith(".jpg")) continue;
const form = new FormData();
form.append("image", new Blob([await fs.promises.readFile(path.join("photos", name))]), name);
const response = await fetch(`${API}/api/v1/mask?method=pixelate&strength=15`, {
method: "POST",
headers: { "X-API-Key": KEY },
body: form,
});
const bytes = Buffer.from(await response.arrayBuffer());
await fs.promises.writeFile(path.join("masked", name), bytes);
console.log(`${name}: ${response.headers.get("X-Face-Count")} faces`);
}
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "xyl_your_api_key_here");
Directory.CreateDirectory("masked");
foreach (var path in Directory.EnumerateFiles("photos", "*.jpg"))
{
using var content = new MultipartFormDataContent();
content.Add(new ByteArrayContent(File.ReadAllBytes(path)), "image", Path.GetFileName(path));
var response = await client.PostAsync(
"https://face-api.xylolabs.com/api/v1/mask?method=pixelate&strength=15",
content
);
var outputPath = Path.Combine("masked", Path.GetFileName(path));
await File.WriteAllBytesAsync(outputPath, await response.Content.ReadAsByteArrayAsync());
Console.WriteLine($"{Path.GetFileName(path)}: {response.Headers.GetValues(\"X-Face-Count\").First()} 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'
import requests
response = requests.post(
"https://face-api.xylolabs.com/api/v1/detect",
headers={"X-API-Key": "xyl_your_key"},
files={"image": open("group.jpg", "rb")},
)
print(response.json()["summary"]["face_count"])
import fs from "fs";
const form = new FormData();
form.append("image", new Blob([fs.readFileSync("group.jpg")]), "group.jpg");
const response = await fetch("https://face-api.xylolabs.com/api/v1/detect", {
method: "POST",
headers: { "X-API-Key": "xyl_your_key" },
body: form,
});
const data = await response.json();
console.log(data.summary.face_count);
using System.Text.Json;
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "xyl_your_key");
using var content = new MultipartFormDataContent();
content.Add(new ByteArrayContent(File.ReadAllBytes("group.jpg")), "image", "group.jpg");
var response = await client.PostAsync("https://face-api.xylolabs.com/api/v1/detect", content);
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Console.WriteLine(doc.RootElement.GetProperty("summary").GetProperty("face_count").GetInt32());
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")
import fs from "fs";
import sharp from "sharp";
const form = new FormData();
form.append("image", new Blob([fs.readFileSync("photo.jpg")]), "photo.jpg");
const response = await fetch("https://face-api.xylolabs.com/api/v1/detect", {
method: "POST",
headers: { "X-API-Key": "xyl_your_key" },
body: form,
});
const { detections } = await response.json();
const svg = `
<svg width="1920" height="1080" xmlns="http://www.w3.org/2000/svg">
${detections.map((face) => {
const { x1, y1, x2, y2 } = face.bbox;
return `<rect x="${x1}" y="${y1}" width="${x2 - x1}" height="${y2 - y1}" fill="none" stroke="red" stroke-width="3" />`;
}).join("")}
</svg>
`;
await sharp("photo.jpg")
.composite([{ input: Buffer.from(svg) }])
.toFile("annotated.jpg");
using System.Text.Json;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "xyl_your_key");
using var content = new MultipartFormDataContent();
content.Add(new ByteArrayContent(File.ReadAllBytes("photo.jpg")), "image", "photo.jpg");
var response = await client.PostAsync("https://face-api.xylolabs.com/api/v1/detect", content);
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
using var image = Image.Load<Rgba32>("photo.jpg");
foreach (var face in doc.RootElement.GetProperty("detections").EnumerateArray())
{
var bbox = face.GetProperty("bbox");
var x1 = bbox.GetProperty("x1").GetSingle();
var y1 = bbox.GetProperty("y1").GetSingle();
var x2 = bbox.GetProperty("x2").GetSingle();
var y2 = bbox.GetProperty("y2").GetSingle();
image.Mutate(ctx => ctx.Draw(Color.Red, 3, new RectangularPolygon(x1, y1, x2 - x1, y2 - y1)));
}
await image.SaveAsJpegAsync("annotated.jpg");
Pipe to S3¶
curl -s -X POST "https://face-api.xylolabs.com/api/v1/mask?format=png" \
-H "X-API-Key: xyl_your_key" \
-F "image=@photo.jpg" | aws s3 cp - s3://my-bucket/masked/photo.png
import io
import boto3
import requests
response = requests.post(
"https://face-api.xylolabs.com/api/v1/mask?format=png",
headers={"X-API-Key": "xyl_your_key"},
files={"image": open("photo.jpg", "rb")},
)
s3 = boto3.client("s3")
s3.upload_fileobj(io.BytesIO(response.content), "my-bucket", "masked/photo.png")
import fs from "fs";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
const form = new FormData();
form.append("image", new Blob([fs.readFileSync("photo.jpg")]), "photo.jpg");
const response = await fetch("https://face-api.xylolabs.com/api/v1/mask?format=png", {
method: "POST",
headers: { "X-API-Key": "xyl_your_key" },
body: form,
});
const s3 = new S3Client({});
await s3.send(new PutObjectCommand({
Bucket: "my-bucket",
Key: "masked/photo.png",
Body: Buffer.from(await response.arrayBuffer()),
}));
using Amazon.S3;
using Amazon.S3.Model;
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "xyl_your_key");
using var content = new MultipartFormDataContent();
content.Add(new ByteArrayContent(File.ReadAllBytes("photo.jpg")), "image", "photo.jpg");
var response = await client.PostAsync("https://face-api.xylolabs.com/api/v1/mask?format=png", content);
var bytes = await response.Content.ReadAsByteArrayAsync();
using var s3 = new AmazonS3Client();
using var stream = new MemoryStream(bytes);
await s3.PutObjectAsync(new PutObjectRequest
{
BucketName = "my-bucket",
Key = "masked/photo.png",
InputStream = stream,
});
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')}")
import fs from "fs";
const form = new FormData();
form.append("image", new Blob([fs.readFileSync("photo.jpg")]), "photo.jpg");
const response = await fetch("https://face-api.xylolabs.com/api/v1/detect", {
method: "POST",
headers: { "X-API-Key": "xyl_your_key" },
body: form,
});
if (response.ok) {
const data = await response.json();
console.log(`${data.summary.face_count} faces`);
} else if (response.status === 413) {
console.log("Image too large");
} else if (response.status === 429) {
console.log(`Rate limited, retry in ${response.headers.get("Retry-After") ?? "60"}s`);
} else if (response.status === 401) {
console.log("Bad API key");
} else {
const data = await response.json();
console.log(`Error ${response.status}: ${data.detail}`);
}
using System.Text.Json;
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "xyl_your_key");
using var content = new MultipartFormDataContent();
content.Add(new ByteArrayContent(File.ReadAllBytes("photo.jpg")), "image", "photo.jpg");
var response = await client.PostAsync("https://face-api.xylolabs.com/api/v1/detect", content);
if (response.IsSuccessStatusCode)
{
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Console.WriteLine(doc.RootElement.GetProperty("summary").GetProperty("face_count").GetInt32());
}
else if ((int)response.StatusCode == 413)
{
Console.WriteLine("Image too large");
}
else if ((int)response.StatusCode == 429)
{
Console.WriteLine($"Rate limited, retry in {response.Headers.GetValues(\"Retry-After\").FirstOrDefault() ?? \"60\"}s");
}
else if ((int)response.StatusCode == 401)
{
Console.WriteLine("Bad API key");
}
else
{
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Console.WriteLine($"Error {(int)response.StatusCode}: {doc.RootElement.GetProperty(\"detail\").GetString()}");
}
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)}%`);
});
using System.Text.Json;
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "xyl_your_key");
using var content = new MultipartFormDataContent();
content.Add(new ByteArrayContent(File.ReadAllBytes("photo.jpg")), "image", "photo.jpg");
var response = await client.PostAsync(
"https://face-api.xylolabs.com/api/v1/detect?confidence=0.7",
content
);
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var summary = doc.RootElement.GetProperty("summary");
Console.WriteLine($"{summary.GetProperty(\"face_count\").GetInt32()} faces");
foreach (var detection in doc.RootElement.GetProperty("detections").EnumerateArray())
{
var bbox = detection.GetProperty("bbox");
Console.WriteLine(
$" #{detection.GetProperty(\"index\").GetInt32()}: " +
$"({(int)bbox.GetProperty(\"x1\").GetSingle()},{(int)bbox.GetProperty(\"y1\").GetSingle()})-" +
$"({(int)bbox.GetProperty(\"x2\").GetSingle()},{(int)bbox.GetProperty(\"y2\").GetSingle()}) " +
$"{detection.GetProperty(\"confidence\").GetDouble() * 100:F1}%"
);
}
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¶
| Constraint | Value |
|---|---|
| Max file size | 20 MB |
| Max pixels | 30,000,000 |
| Min size | 10 x 10 px |
| Formats | JPEG, PNG, WebP, BMP, TIFF |