Skip to content

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.

X-API-Key: xyl_a1b2c3d4e5f6...

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.

curl https://face-api.xylolabs.com/health
{
  "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

curl -X POST https://face-api.xylolabs.com/api/v1/detect \
  -H "X-API-Key: xyl_your_api_key_here" \
  -F "image=@photo.jpg" \
  -F "confidence=0.6" \
  -F "max_faces=5"
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)`);
import requests

with open("photo.jpg", "rb") as f:
    image_bytes = f.read()

response = requests.post(
    "https://face-api.xylolabs.com/api/v1/detect",
    headers={"X-API-Key": "xyl_your_api_key_here"},
    files={"image": ("photo.jpg", image_bytes, "image/jpeg")},
)

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

curl -X POST https://face-api.xylolabs.com/api/v1/mask \
  -H "X-API-Key: xyl_your_api_key_here" \
  -F "image=@photo.jpg" \
  -F "method=pixelate" \
  -F "strength=20" \
  -o masked.jpg
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
{ "detail": "Error description" }

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