Skip to content

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).

X-API-Key: xyl_a1b2c3d4e5f6...

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

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

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, or image/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

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

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