Facial Analysis API

Integrate apparent-age estimation, a 478-point face mesh in MediaPipe-compatible format, or both through a versioned JSON API.

Overview

The API accepts JSON requests and analyzes the primary face in an image. Every successful response includes api_version; every failure uses the same structured error envelope.

Base URL: https://api.rate-my-photo.com. Contact sales before production use to discuss access and pricing.

Authentication required

Send your API token as Authorization: Bearer <token> and use Content-Type: application/json. The examples read the token from an API_TOKEN environment variable.

Method Path Purpose Credit cost
POST /age Estimate age; optionally return a boundary box, face crop, and face mesh. 3 credits
POST /facemesh Return facial keypoints; optionally return frontalized keypoints and pose. 2 credits

Image input

Both endpoints require exactly one of the following request fields:

Field Type Description
face_image string Raw base64 or a data:image/...;base64, data URI.
face_image_url string A public HTTP or HTTPS image URL. Private, loopback, link-local, and unreachable targets are refused.

Limits and preprocessing

  • Decoded image size must not exceed 15 MB.
  • Neither image dimension may exceed 4500 px.
  • If the longest side is above 1000 px, the image is downscaled to a maximum side of 1000 px with aspect ratio preserved.
  • For a combined request, age and face mesh analysis run on that same processed image.

Age estimation

POST /age

Estimates the apparent age of the primary face.

Credit usage

Each successful Age API request consumes 3 credits.

Basic request

#!/usr/bin/env bash
set -euo pipefail
: "${API_TOKEN:?Set the API_TOKEN environment variable}"

curl -sS --fail-with-body https://api.rate-my-photo.com/age \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "face_image_url": "https://howolddoyoulook.com/static/images/sample/portrait_example_male.jpg",
    "return_boundary_box": true,
    "return_face_base64": false,
    "return_facemesh": false
  }'

Request parameters

In addition to one image source, the endpoint accepts these optional controls:

Parameter Type Default Effect
return_boundary_box boolean true Include x, y, width, and height.
return_face_base64 boolean false Include a base64-encoded crop of the detected face.

Response schema

Fields marked conditional appear only when their corresponding request parameter is enabled.

Field Type Presence Description
api_version string Always The effective API version used for the request.
age number Always Estimated apparent age in years.
boundary_box object Conditional Face position as pixel values x, y, width, and height. Returned when return_boundary_box is true.
face_base64 string Conditional Base64-encoded JPEG crop of the detected face. Returned when return_face_base64 is true.
facemesh object Conditional Nested face mesh result using the schema documented below. Returned when return_facemesh is true.

The boundary box uses the coordinate space of the processed image described under Image input.

Example response

{
  "api_version": "1.1",
  "age": 25.42,
  "boundary_box": {
    "x": 113,
    "y": 89,
    "width": 142,
    "height": 180
  }
}

Save the returned face

Set return_face_base64 to true and the response includes face_base64, a Base64-encoded JPEG crop of the detected face. Decode that string and write it to disk, as shown below. Alternatively, leave return_boundary_box enabled and crop the face yourself from the original image using the returned coordinates.

import base64
from pathlib import Path

data = response.json()  # response from POST /age with "return_face_base64": true
OUTPUT_PATH = Path("age_face.jpg")
face_base64 = data.get("face_base64")
if face_base64:
    OUTPUT_PATH.write_bytes(base64.b64decode(face_base64))
    print(f"Saved face image to: {OUTPUT_PATH.resolve()}")
else:
    print("No face_base64 in response.")
Face crop decoded from the API's face_base64 field
The decoded face_base64 crop written to age_face.jpg.

Face mesh

POST /facemesh

Returns 478 normalized three-dimensional landmarks in a MediaPipe-compatible face-mesh format for the primary face.

Credit usage

Each successful Facemesh API request consumes 2 credits.

Basic request

#!/usr/bin/env bash
set -euo pipefail
: "${API_TOKEN:?Set the API_TOKEN environment variable}"

curl -sS --fail-with-body https://api.rate-my-photo.com/facemesh \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "face_image_url": "https://howolddoyoulook.com/static/images/sample/portrait_example_male.jpg",
    "return_keypoints": true,
    "return_frontalized_keypoints": true,
    "return_pose": true
  }'

Request parameters

In addition to one image source, the endpoint accepts these optional output controls:

Parameter Type Default Effect
return_keypoints boolean true Include the 478 x, y, and z keypoints.
return_frontalized_keypoints boolean false Include 478 frontalized keypoints.
return_pose boolean false Include head-pose measurements such as yaw, pitch, and roll.

Response schema

All optional fields are controlled independently by the request parameters above.

Field Type Presence Description
api_version string Always The effective API version used for the request.
keypoints array<object> Conditional 478 normalized landmarks. Each item contains numeric x, y, and z values. Returned when return_keypoints is true.
frontalized_keypoints array<object> Conditional 478 pose-corrected landmarks with numeric x, y, and z values. Returned when return_frontalized_keypoints is true.
pose object Conditional Head-pose measurements: yaw, pitch, roll, off_frontal_deg, and frontal_deviation. Returned when return_pose is true.

Example response

{
  "api_version": "1.1",
  "keypoints": [
    { "x": 0.47999, "y": 0.58708, "z": -0.04268 }
  ],
  "frontalized_keypoints": [
    { "x": 0.48012, "y": 0.58191, "z": -0.03924 }
  ],
  "pose": {
    "yaw": 1.2,
    "pitch": -2.4,
    "roll": 0.8,
    "off_frontal_deg": 2.8,
    "frontal_deviation": 0.03
  }
}

The arrays are abbreviated for readability. A full keypoint response contains 478 entries. This endpoint does not return a boundary box.

Visualize the face mesh

This short Python example calls POST /age, crops the face with the returned boundary box, draws the 478 keypoints on it, and compares that with the frontalized_keypoints side by side. Keypoints are normalized to the full image (0..1); frontalized keypoints are returned in a centered, pose-corrected space, so they are simply scaled to fit their panel.

Requires pip install requests pillow. Save a portrait as c.jpg next to the script.

Python example

import base64
            import json
            import os
            from pathlib import Path
            
            import requests
            from PIL import Image, ImageDraw, ImageFont, ImageOps
            
            API_URL = "https://api.rate-my-photo.com/age"
            IMAGE_PATH = Path("api-examples/demo-photos/img2.png")
            OUTPUT_PATH = Path("facemesh_comparison.png")
            
            
            def get_font(size):
                return ImageFont.load_default()
            
            
            def draw_points(image, points, color):
                draw = ImageDraw.Draw(image)
                radius = max(1.5, min(image.size) / 300)
                for x, y in points:
                    draw.ellipse((x - radius - 1, y - radius - 1, x + radius + 1, y + radius + 1), fill="white")
                    draw.ellipse((x - radius, y - radius, x + radius, y + radius), fill=color)
            
            
            def add_title(image, title, color):
                panel = Image.new("RGB", (image.width, image.height + 55), "white")
                panel.paste(image, (0, 55))
                draw = ImageDraw.Draw(panel)
                draw.text((18, 16), title, fill=color, font=get_font(19))
                draw.rectangle((0, 0, panel.width - 1, panel.height - 1), outline="#dce1e8", width=2)
                return panel
            
            
            token = os.getenv("API_TOKEN")
            if not token:
                raise RuntimeError("Set the API_TOKEN environment variable.")
            if not IMAGE_PATH.exists():
                raise FileNotFoundError(f"Image not found: {IMAGE_PATH}")
            
            image = ImageOps.exif_transpose(Image.open(IMAGE_PATH)).convert("RGB")
            width, height = image.size
            encoded = base64.b64encode(IMAGE_PATH.read_bytes()).decode()
            
            response = requests.post(API_URL, headers={
                "Authorization": f"Bearer {token}",
                "X-API-Version": "1.1",
            }, json={
                "face_image": encoded,
                "return_boundary_box": True,
                "return_facemesh": True,
                "return_frontalized_keypoints": True,
            }, timeout=60)
            
            print(f"Status code: {response.status_code}")
            try:
                data = response.json()
                print(json.dumps(data, indent=2))
            except requests.JSONDecodeError:
                print(response.text)
                raise RuntimeError("API returned invalid JSON.")
            
            response.raise_for_status()
            
            box = data["boundary_box"]
            keypoints = data["facemesh"]["keypoints"]
            frontalized = data["facemesh"]["frontalized_keypoints"]
            
            padding_x, padding_y = box["width"] * 0.12, box["height"] * 0.12
            left = max(0, round(box["x"] - padding_x))
            top = max(0, round(box["y"] - padding_y))
            right = min(width, round(box["x"] + box["width"] + padding_x))
            bottom = min(height, round(box["y"] + box["height"] + padding_y))
            
            face = image.crop((left, top, right, bottom))
            draw_points(face, [(p["x"] * width - left, p["y"] * height - top) for p in keypoints], "#22c55e")
            
            xs, ys = [p["x"] for p in frontalized], [p["y"] for p in frontalized]
            min_x, max_x, min_y, max_y = min(xs), max(xs), min(ys), max(ys)
            mesh_width, mesh_height = max_x - min_x, max_y - min_y
            pad = max(25, round(min(face.size) * 0.1))
            scale = min((face.width - 2 * pad) / mesh_width, (face.height - 2 * pad) / mesh_height)
            offset_x = (face.width - mesh_width * scale) / 2
            offset_y = (face.height - mesh_height * scale) / 2
            
            front = Image.new("RGB", face.size, "#f8fafc")
            draw_points(front, [
                (offset_x + (p["x"] - min_x) * scale, offset_y + (p["y"] - min_y) * scale)
                for p in frontalized
            ], "#3b82f6")
            
            face = add_title(face, "Detected face mesh", "#166534")
            front = add_title(front, "Frontalized face mesh", "#1e40af")
            
            gap, margin = 24, 28
            canvas = Image.new("RGB", (face.width + front.width + gap + margin * 2, face.height + margin * 2), "#f1f5f9")
            canvas.paste(face, (margin, margin))
            canvas.paste(front, (margin + face.width + gap, margin))
            canvas.save(OUTPUT_PATH, quality=95)
            
            print(f"Saved result to: {OUTPUT_PATH.resolve()}")

Result

Left: the cropped face with the original mesh. Right: the same face mesh frontalized to a straight-on view.

Original face mesh next to the frontalized face mesh
Original face mesh (left) compared with the frontalized face mesh (right).

Error handling

Errors use an HTTP status appropriate to the failure and a stable machine-readable code.

{
  "api_version": "1.1",
  "error": {
    "code": "NO_FACE_DETECTED",
    "message": "No face was detected in the supplied image."
  }
}
Status Code Meaning
400 VALIDATION_ERROR Missing or conflicting image sources, invalid fields, or an invalid URL.
400 UNSUPPORTED_VERSION The requested API version is not supported.
413 IMAGE_TOO_LARGE The decoded image exceeds 15 MB.
413 IMAGE_DIMENSIONS_TOO_LARGE An image side exceeds 4500 px.
422 INVALID_IMAGE The image or base64 data cannot be decoded.
422 NO_FACE_DETECTED No usable face was found.
422 IMAGE_URL_UNREACHABLE The URL failed or resolved to a refused address.
502 UPSTREAM_ERROR An analysis service returned an unexpected response.
504 UPSTREAM_TIMEOUT An analysis service did not respond in time.
500 INTERNAL_ERROR An unexpected server error occurred.

Versioning

Set the optional X-API-Version request header. If the header is omitted, the API defaults to version 1.1. The effective version is echoed in the response header and JSON body.

X-API-Version: 1.1

An unsupported value returns HTTP 400 with error code UNSUPPORTED_VERSION.

Planning a production integration?

Tell us about your use case, expected traffic, and required response fields.