> ## Documentation Index
> Fetch the complete documentation index at: https://docs.didit.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Age Estimation API

> Estimate a person's age from a single face photo and run a passive liveness check in the same call — returns the model-predicted `age_estimation` (in years), a liveness `score` (0–100), and an `Approved`/`Declined` `status`. The age is **model-predicted from the face, not verified against a document** — use it for age gates and low-friction checks, not as legal proof of age.

**How it works.** The image is analyzed for faces (set `rotate_image=true` if captures may be sideways or upside down); each detected face gets an estimated age, and the largest face in the frame drives `age_estimation`. In parallel, a passive liveness model scores whether the photo shows a live person (no user challenge required) and detects presentation attacks (photos of photos, screens, masks). Submit one clear, front-facing, well-lit face per image for best results.

**Decision logic.** Any warning declines: `status` is `Approved` only when the `warnings` array is empty. A liveness `score` at or below `face_liveness_score_decline_threshold` (default `30`) adds `LOW_LIVENESS_SCORE`; a detected attack adds `LIVENESS_FACE_ATTACK`; no face found by the liveness model adds `NO_FACE_DETECTED`; no estimable face age adds `AGE_NOT_DETECTED` (`age_estimation` is `null`); an estimated age below `age_estimation_decline_threshold` (default `18`) adds `AGE_BELOW_MINIMUM`. Borderline ages near your threshold deserve a fallback to document-based verification — the model's error grows for faces close to the limit.

**Billing.** Each `200` response consumes one Age Estimation API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing.

**Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/` (the result is exposed there in `liveness_checks[]`, with `features: ["AGE_ESTIMATION"]`), the face crop and reference image are stored, the face embedding is added to your application's face index (used by face search and duplicate detection), and a `status.updated` webhook is emitted. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later.

**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (`age_estimation: 30`, no warnings), no session is persisted, and no credits are consumed.

**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) — this API never returns `401`.

**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.



## OpenAPI

````yaml POST /v3/age-estimation/
openapi: 3.0.0
info:
  version: 3.0.0
  title: Didit Verification API
  description: Identity verification API. Authenticate with x-api-key header.
servers:
  - url: https://verification.didit.me
security: []
tags: []
paths:
  /v3/age-estimation/:
    post:
      tags:
        - Standalone APIs
      summary: Age Estimation (face age + passive liveness)
      description: >-
        Estimate a person's age from a single face photo and run a passive
        liveness check in the same call — returns the model-predicted
        `age_estimation` (in years), a liveness `score` (0–100), and an
        `Approved`/`Declined` `status`. The age is **model-predicted from the
        face, not verified against a document** — use it for age gates and
        low-friction checks, not as legal proof of age.


        **How it works.** The image is analyzed for faces (set
        `rotate_image=true` if captures may be sideways or upside down); each
        detected face gets an estimated age, and the largest face in the frame
        drives `age_estimation`. In parallel, a passive liveness model scores
        whether the photo shows a live person (no user challenge required) and
        detects presentation attacks (photos of photos, screens, masks). Submit
        one clear, front-facing, well-lit face per image for best results.


        **Decision logic.** Any warning declines: `status` is `Approved` only
        when the `warnings` array is empty. A liveness `score` at or below
        `face_liveness_score_decline_threshold` (default `30`) adds
        `LOW_LIVENESS_SCORE`; a detected attack adds `LIVENESS_FACE_ATTACK`; no
        face found by the liveness model adds `NO_FACE_DETECTED`; no estimable
        face age adds `AGE_NOT_DETECTED` (`age_estimation` is `null`); an
        estimated age below `age_estimation_decline_threshold` (default `18`)
        adds `AGE_BELOW_MINIMUM`. Borderline ages near your threshold deserve a
        fallback to document-based verification — the model's error grows for
        faces close to the limit.


        **Billing.** Each `200` response consumes one Age Estimation API credit
        (standalone APIs have no free tier). When the organization's balance
        cannot cover the call, the endpoint returns `403` with the
        not-enough-credits error before any image processing.


        **Session persistence (`save_api_request`, default `true`).** When
        `true`, the call is persisted as an API-type session: it appears in the
        Business Console, the returned `request_id` is a real session id you can
        pass to `GET /v3/session/{sessionId}/decision/` (the result is exposed
        there in `liveness_checks[]`, with `features: ["AGE_ESTIMATION"]`), the
        face crop and reference image are stored, the face embedding is added to
        your application's face index (used by face search and duplicate
        detection), and a `status.updated` webhook is emitted. When `false`,
        nothing is stored and `request_id` is a one-off correlation UUID that
        cannot be looked up later.


        **Sandbox.** Sandbox API keys skip all processing and billing: after
        request validation (malformed input still returns `400`), the endpoint
        returns a static `Approved` mock payload (`age_estimation: 30`, no
        warnings), no session is persisted, and no credits are consumed.


        **Authentication.** Send your application's API key in the `x-api-key`
        header. Missing or invalid credentials return `403` (`{"detail": "You do
        not have permission to perform this action."}`) — this API never returns
        `401`.


        **Rate limit.** Shared write budget of 300 requests/min per API key
        across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.
      operationId: post_v3age-estimation
      parameters: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - user_image
              properties:
                user_image:
                  type: string
                  format: binary
                  description: >-
                    Face photo to analyze (e.g. a selfie). Submit a single
                    front-facing photo with the subject's face clearly visible
                    and well lit. Allowed extensions: `tiff`, `jpg`, `jpeg`,
                    `png`, `webp` (no PDF). Maximum upload size: **5 MB**
                    (larger files are rejected with `400`). Images are
                    automatically compressed to ~0.3 MB before processing, so
                    very high resolutions do not improve accuracy.
                face_liveness_score_decline_threshold:
                  type: number
                  format: float
                  default: 30
                  minimum: 0
                  maximum: 100
                  description: >-
                    Liveness threshold (0–100). A liveness `score` **at or
                    below** this value adds a `LOW_LIVENESS_SCORE` warning and
                    declines. Raise it for stricter anti-spoofing; values
                    outside 0–100 return `400`.
                  example: 30
                age_estimation_decline_threshold:
                  type: integer
                  default: 18
                  minimum: 0
                  maximum: 100
                  description: >-
                    Minimum acceptable estimated age in years. An estimated age
                    **below** this value adds an `AGE_BELOW_MINIMUM` warning and
                    declines. Set `0` to disable the age check (the call then
                    only runs liveness + age estimation without an age-based
                    decision).
                  example: 18
                rotate_image:
                  type: boolean
                  default: false
                  description: >-
                    When `true`, the service tries the input image in 90-degree
                    increments (0, 90, 180, 270) and uses the orientation that
                    yields the best face detection — useful for mobile captures
                    with missing EXIF orientation. Adds latency, so only enable
                    it if you cannot guarantee upright images.
                  example: false
                save_api_request:
                  type: boolean
                  default: true
                  description: >-
                    When `true` (default), persists the call as an API-type
                    session — visible in the Business Console, retrievable via
                    `GET /v3/session/{sessionId}/decision/` using the returned
                    `request_id`, announced through a `status.updated` webhook,
                    and with the detected face added to your application's face
                    index. When `false`, nothing is stored and `request_id` is a
                    transient UUID for response correlation only.
                  example: true
                vendor_data:
                  type: string
                  description: >-
                    Optional opaque string (your internal user id, email, UUID…)
                    stored on the persisted session and echoed back in the
                    response. Use it to correlate API calls with your own
                    records and to filter sessions later.
                  example: user-123
                metadata:
                  type: object
                  additionalProperties: true
                  description: >-
                    Optional JSON object stored with the session (when
                    `save_api_request=true`) and echoed back in the response. In
                    multipart requests, send it as a JSON-encoded string field
                    (e.g. `metadata={"flow":"age-gate"}`) — it is parsed into an
                    object.
                  example:
                    flow: age-gate
            example:
              user_image: (binary JPEG/PNG of the selfie)
              face_liveness_score_decline_threshold: 30
              age_estimation_decline_threshold: 18
              rotate_image: false
              save_api_request: true
              vendor_data: user-123
              metadata:
                flow: age-gate
      responses:
        '200':
          description: >-
            Age estimation and passive liveness completed.
            `age_estimation.age_estimation` is the model-predicted age in years
            of the largest detected face (`null` if none);
            `age_estimation.score` is the passive-liveness score (0–100);
            `status` is `Approved` only when `warnings` is empty. An underage or
            spoofed capture still returns `200` with `status: "Declined"` —
            inspect the body, not just the HTTP code. When
            `save_api_request=true`, `request_id` is the persisted session id.
          content:
            application/json:
              examples:
                Approved:
                  summary: Live adult face — both checks passed
                  value:
                    request_id: 0c40ba43-64ab-4e2e-b4b8-7d1f12f81bc1
                    age_estimation:
                      status: Approved
                      method: PASSIVE
                      score: 97.5
                      user_image:
                        entities:
                          - age: 27.33
                            bbox:
                              - 40
                              - 40
                              - 100
                              - 100
                            confidence: 0.7177750468254089
                            gender: male
                        best_angle: 0
                      age_estimation: 27.33
                      warnings: []
                    vendor_data: user-123
                    metadata:
                      flow: age-gate
                    created_at: '2026-06-12T02:24:11.512941+00:00'
                Declined - age below threshold:
                  summary: Estimated age below `age_estimation_decline_threshold`
                  value:
                    request_id: 9be41a6f-2f4e-4f57-92f4-b3a4f6f0a1c2
                    age_estimation:
                      status: Declined
                      method: PASSIVE
                      score: 95
                      user_image:
                        entities:
                          - age: 16.5
                            bbox:
                              - 40
                              - 40
                              - 100
                              - 100
                            confidence: 0.7177750468254089
                            gender: female
                        best_angle: 0
                      age_estimation: 16.5
                      warnings:
                        - risk: AGE_BELOW_MINIMUM
                          feature: LIVENESS
                          additional_data: null
                          log_type: error
                          short_description: Age below minimum
                          long_description: >-
                            The age of the face is below the minimum age
                            threshold for the application.
                    vendor_data: user-123
                    metadata: null
                    created_at: '2026-06-12T02:26:40.118332+00:00'
                Declined - liveness too low:
                  summary: >-
                    Liveness score at or below the decline threshold (possible
                    spoof)
                  value:
                    request_id: 5f6f2f1f-7c4f-43b9-8d62-0a8f4c2d9e77
                    age_estimation:
                      status: Declined
                      method: PASSIVE
                      score: 5
                      user_image:
                        entities:
                          - age: 25
                            bbox:
                              - 40
                              - 40
                              - 100
                              - 100
                            confidence: 0.7177750468254089
                            gender: male
                        best_angle: 0
                      age_estimation: 25
                      warnings:
                        - risk: LOW_LIVENESS_SCORE
                          feature: LIVENESS
                          additional_data: null
                          log_type: error
                          short_description: Low liveness score
                          long_description: >-
                            The liveness check resulted in a low score,
                            indicating potential use of non-live facial
                            representations or poor-quality biometric data.
                    vendor_data: user-123
                    metadata: null
                    created_at: '2026-06-12T02:27:02.901547+00:00'
              schema:
                type: object
                properties:
                  request_id:
                    type: string
                    format: uuid
                    description: >-
                      Persisted session id when `save_api_request=true` (usable
                      with `GET /v3/session/{sessionId}/decision/`); otherwise a
                      transient correlation UUID.
                  age_estimation:
                    type: object
                    properties:
                      status:
                        type: string
                        enum:
                          - Approved
                          - Declined
                        description: >-
                          `Approved` when no warnings were raised; `Declined`
                          otherwise (every warning this endpoint produces
                          declines).
                      method:
                        type: string
                        enum:
                          - PASSIVE
                        description: >-
                          Liveness method. Always `PASSIVE` on this endpoint
                          (single-image, no user challenge).
                      score:
                        type: number
                        format: float
                        nullable: true
                        minimum: 0
                        maximum: 100
                        description: >-
                          Passive-liveness score from 0 to 100 — the model's
                          confidence that the photo shows a live person.
                          Compared against
                          `face_liveness_score_decline_threshold`; at or below
                          it the call declines with `LOW_LIVENESS_SCORE`. `null`
                          when the liveness model returns no confidence — the
                          call still declines with `LOW_LIVENESS_SCORE` because
                          null is treated as 0 for the threshold comparison.
                        example: 97.5
                      user_image:
                        type: object
                        description: Face-detection results for the analyzed image.
                        properties:
                          entities:
                            type: array
                            description: >-
                              One entry per detected face. Empty when no face
                              was found (then `age_estimation` is `null` and an
                              `AGE_NOT_DETECTED` warning is added).
                            items:
                              type: object
                              properties:
                                age:
                                  type: number
                                  format: float
                                  description: Model-estimated age of this face, in years.
                                  example: 27.33
                                bbox:
                                  type: array
                                  items:
                                    type: integer
                                  minItems: 4
                                  maxItems: 4
                                  description: >-
                                    Bounding box of the detected face as
                                    `[x_min, y_min, x_max, y_max]` pixel
                                    coordinates in the processed image.
                                  example:
                                    - 40
                                    - 40
                                    - 100
                                    - 100
                                confidence:
                                  type: number
                                  format: float
                                  minimum: 0
                                  maximum: 1
                                  description: Face-detection confidence (0–1).
                                  example: 0.7177750468254089
                                gender:
                                  type: string
                                  description: >-
                                    Model-predicted gender of the detected face
                                    (`male` or `female`). Informational only.
                                  example: male
                          best_angle:
                            type: integer
                            nullable: true
                            description: >-
                              Rotation (degrees: 0, 90, 180, or 270) that
                              produced the best face detection. Only non-zero
                              when `rotate_image=true` corrected the
                              orientation.
                            example: 0
                      age_estimation:
                        type: number
                        format: float
                        nullable: true
                        description: >-
                          Model-predicted age in years of the **largest**
                          detected face. `null` when no face age could be
                          estimated (an `AGE_NOT_DETECTED` warning is added and
                          the call declines).
                        example: 27.33
                      warnings:
                        type: array
                        description: >-
                          Empty on a clean approval. Any entry sets `status` to
                          `Declined`.
                        items:
                          type: object
                          properties:
                            risk:
                              type: string
                              enum:
                                - NO_FACE_DETECTED
                                - LOW_LIVENESS_SCORE
                                - LIVENESS_FACE_ATTACK
                                - AGE_NOT_DETECTED
                                - AGE_BELOW_MINIMUM
                              description: >-
                                Machine-readable risk code. `NO_FACE_DETECTED` —
                                the liveness model found no face;
                                `LOW_LIVENESS_SCORE` — liveness `score` at or
                                below `face_liveness_score_decline_threshold`;
                                `LIVENESS_FACE_ATTACK` — a presentation attack
                                (photo/screen/mask) was detected;
                                `AGE_NOT_DETECTED` — no face age could be
                                estimated; `AGE_BELOW_MINIMUM` — estimated age
                                below `age_estimation_decline_threshold`.
                                `NO_FACE_DETECTED` and `LOW_LIVENESS_SCORE` are
                                mutually exclusive — when no face is found only
                                `NO_FACE_DETECTED` is emitted.
                            feature:
                              type: string
                              enum:
                                - LIVENESS
                              description: >-
                                Feature that raised the warning. Always
                                `LIVENESS` on this endpoint.
                            additional_data:
                              type: object
                              nullable: true
                              additionalProperties: true
                              description: Always `null` for age-estimation warnings.
                            log_type:
                              type: string
                              enum:
                                - error
                              description: >-
                                Severity. Every warning this endpoint produces
                                is an `error` and sets `status` to `Declined`.
                            short_description:
                              type: string
                              description: Human-readable one-line summary of the risk.
                            long_description:
                              type: string
                              description: Human-readable explanation of the risk.
                  vendor_data:
                    type: string
                    nullable: true
                    description: Echo of the `vendor_data` you sent, or `null`.
                  metadata:
                    type: object
                    nullable: true
                    additionalProperties: true
                    description: Echo of the `metadata` object you sent, or `null`.
                  created_at:
                    type: string
                    format: date-time
                    description: >-
                      ISO 8601 timestamp (UTC) of when the response was
                      generated.
        '400':
          description: >-
            Validation error. Returned when `user_image` is missing, exceeds 5
            MB, has an unsupported extension, or when a threshold is out of
            range. The body is DRF's standard field-error envelope: one array of
            messages per offending field.
          content:
            application/json:
              examples:
                Missing user image:
                  summary: '`user_image` not included in the form data'
                  value:
                    user_image:
                      - No file was submitted.
                Unsupported file extension:
                  summary: File extension outside tiff/jpg/jpeg/png/webp
                  value:
                    user_image:
                      - >-
                        File extension “txt” is not allowed. Allowed extensions
                        are: tiff, jpg, jpeg, png, webp.
                File too large:
                  summary: Upload exceeds the 5 MB limit
                  value:
                    user_image:
                      - File size should not exceed 5 MB
                Threshold out of range:
                  summary: '`face_liveness_score_decline_threshold` outside 0–100'
                  value:
                    face_liveness_score_decline_threshold:
                      - Ensure this value is less than or equal to 100.
        '403':
          description: >-
            Permission denied. Returned when the `x-api-key` header is missing,
            malformed, revoked, or belongs to another environment — and also
            when the calling organization's balance cannot cover the call.
            Authentication failures return `403` with `{"detail": ...}`; this
            API never returns `401`. Credit shortfalls return `403` with
            `{"error": ...}` before any image processing happens.
          content:
            application/json:
              examples:
                Missing or invalid API key:
                  summary: No `x-api-key` header, or the key is invalid/revoked
                  value:
                    detail: You do not have permission to perform this action.
                Not enough credits:
                  summary: Organization balance cannot cover the call
                  value:
                    error: >-
                      You don't have enough credits to perform this request.
                      Please top up at https://business.didit.me
        '429':
          description: >-
            Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget
            of 300 write requests per minute per API key. The response carries
            `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`,
            and `Retry-After` headers.
          content:
            application/json:
              examples:
                Write rate limit exceeded:
                  summary: More than 300 write requests in the current minute
                  value:
                    detail: >-
                      Write request rate limit exceeded. You can make up to 300
                      requests per minute.
              schema:
                type: object
                properties:
                  detail:
                    type: string
      security:
        - ApiKeyAuth: []
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl -X POST 'https://verification.didit.me/v3/age-estimation/' \
              -H 'x-api-key: YOUR_API_KEY' \
              -F 'user_image=@./selfie.jpg' \
              -F 'age_estimation_decline_threshold=18' \
              -F 'face_liveness_score_decline_threshold=30' \
              -F 'save_api_request=true' \
              -F 'vendor_data=user-123'
        - lang: python
          label: Python
          source: |-
            import requests

            url = 'https://verification.didit.me/v3/age-estimation/'
            headers = {'x-api-key': 'YOUR_API_KEY'}

            with open('selfie.jpg', 'rb') as f:
                files = {'user_image': ('selfie.jpg', f, 'image/jpeg')}
                data = {
                    'age_estimation_decline_threshold': 18,
                    'face_liveness_score_decline_threshold': 30,
                    'save_api_request': 'true',
                    'vendor_data': 'user-123',
                }
                resp = requests.post(url, headers=headers, files=files, data=data, timeout=60)

            resp.raise_for_status()
            result = resp.json()['age_estimation']
            print('status:', result['status'])
            print('estimated age:', result['age_estimation'])
            print('liveness score:', result['score'])
            for w in result['warnings']:
                print('warning:', w['risk'])
        - lang: javascript
          label: JavaScript
          source: >-
            import fs from 'node:fs';


            const form = new FormData();

            form.append('user_image', new
            Blob([fs.readFileSync('./selfie.jpg')]), 'selfie.jpg');

            form.append('age_estimation_decline_threshold', '18');

            form.append('face_liveness_score_decline_threshold', '30');

            form.append('save_api_request', 'true');

            form.append('vendor_data', 'user-123');


            const response = await
            fetch('https://verification.didit.me/v3/age-estimation/', {
              method: 'POST',
              headers: { 'x-api-key': 'YOUR_API_KEY' },
              body: form,
            });


            if (!response.ok) throw new Error(`Age estimation failed:
            ${response.status}`);

            const { age_estimation } = await response.json();

            console.log('status:', age_estimation.status, 'age:',
            age_estimation.age_estimation, 'liveness:', age_estimation.score);
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key

````