> This page is for Expression Measurement.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://dev.hume.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://dev.hume.ai/_mcp/server.

# Image upload

**The image upload endpoint measures emotional expression on faces in one or more JPEG images.** Send the images in one request, and the response has one result per image, listing the faces found in it. A [realtime session](/expression-measurement/docs/video/realtime) returns the same measurements for the same images.

<table>
  <tbody>
    <tr>
      <th width="20%">
        Endpoint
      </th>

      <td>
        `POST https://api.cloud.hume.ai/v1/expression/video/file`
      </td>
    </tr>

    <tr>
      <th>
        Reference
      </th>

      <td>
        [Upload images](/expression-measurement/reference/video/measure)
      </td>
    </tr>

    <tr>
      <th>
        Content type
      </th>

      <td>
        `multipart/form-data`
      </td>
    </tr>

    <tr>
      <th>
        Run
      </th>

      <td>
        Recorded under the 

        `run_id`

        . See 

        [Runs](/expression-measurement/docs/runs)

        .
      </td>
    </tr>
  </tbody>
</table>

## Request

The request body has one or more `file` parts and an optional `config` part. Authenticate with a credential header, as described in [Authentication](/expression-measurement/docs/authentication).

**`file`** — required

One JPEG image per part, sent with the content type `image/jpeg`. Repeat the part to send several images.

---

**`config`**

Settings as a JSON object, sent with the content type `application/json`. See [Configuration](#configuration). Omit it to use the defaults.

---

The request below uploads two images, lowers the detection threshold, and asks for a `face_state` to continue face tracking in a later request.

#### cURL

```bash
curl https://api.cloud.hume.ai/v1/expression/video/file \
  -H "X-Hume-Api-Key: $HUME_API_KEY" \
  -F "file=@frame-0.jpg;type=image/jpeg" \
  -F "file=@frame-1.jpg;type=image/jpeg" \
  -F 'config={
    "face": {
      "threshold": 0.8,
      "return_face_state": true
    }
  };type=application/json'
```

#### Python

```python
from hume_expression_measurement import ExpressionMeasurementClient, VideoFileConfig, VideoFileConfigFace

client = ExpressionMeasurementClient()

with open("frame-0.jpg", "rb") as frame_0, open("frame-1.jpg", "rb") as frame_1:
    response = client.video.measure(
        file=[
            ("frame-0.jpg", frame_0, "image/jpeg"),
            ("frame-1.jpg", frame_1, "image/jpeg"),
        ],
        config=VideoFileConfig(
            face=VideoFileConfigFace(threshold=0.8, return_face_state=True),
        ),
    )

print(f"{len(response.measurements)} images, face_state {response.face_state}")
```

#### Node.js

```javascript
import { ExpressionMeasurementClient } from "@humeai/expression-measurement-node";

const client = new ExpressionMeasurementClient();

const response = await client.video.measure({
  file: [
    {
      path: "frame-0.jpg",
      contentType: "image/jpeg",
    },
    {
      path: "frame-1.jpg",
      contentType: "image/jpeg",
    },
  ],
  config: {
    face: {
      threshold: 0.8,
      returnFaceState: true,
    },
  },
});

console.log(`${response.measurements.length} images, faceState ${response.faceState}`);
```

## Images

Each `file` part holds one complete JPEG image.

<table>
  <thead>
    <tr>
      <th width="28%">
        Limit
      </th>

      <th>
        Value
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        Format
      </td>

      <td>
        JPEG
      </td>
    </tr>

    <tr>
      <td>
        Image size
      </td>

      <td>
        2 MB
      </td>
    </tr>

    <tr>
      <td>
        Pixels per image
      </td>

      <td>
        8,294,400 (3840x2160)
      </td>
    </tr>

    <tr>
      <td>
        Request size
      </td>

      <td>
        25 MB for the whole multipart body
      </td>
    </tr>

    <tr>
      <td>
        Faces measured per image
      </td>

      <td>
        2 by default, the largest. Contact support to raise it.
      </td>
    </tr>

    <tr>
      <td>
        Faces listed per image
      </td>

      <td>
        32
      </td>
    </tr>
  </tbody>
</table>

The server checks every image before measuring any. An image that is not a JPEG, or that exceeds the size or pixel limit, fails the request with `unsupported_media_type` or `image_too_large`. After earlier images are measured, an image that cannot be decoded can still fail the request with `image_decode_failed`. A request that fails in any of these ways returns no result for any image and is recorded as an `errored` run.

## Detection

The server scans each image for faces and excludes any whose detection confidence is below `face.threshold` or whose bounding box is shorter than `face.min_size` pixels on its shorter side. It measures the largest of the remaining faces, up to 2 by default, and lists them first in `faces`, largest first. The faces that were not measured follow, most confident first, up to 32 faces in all.

A face that was not measured carries its `bbox` and `confidence`, with `face_id`, `expressions`, and `descriptions` set to `null`. The limit on measured faces is set for your organization and does not appear in responses. To raise it, contact support.

## Response

The response below is for two images. Three faces appear in the first, and the two largest are measured. One of the measured faces remains in the second.

#### 200 OK

```json
{
  "request_id": "8b27e4d1c05f4a93b6e0d7a2c91f3e58",
  "run_id": "01926f3e-397d-7ceb-9f52-a864c0102a44",
  "received": {
    "frames": 2
  },
  "measurements": [
    {
      "frame_id": 0,
      "faces": [
        {
          "face_id": 0,
          "bbox": [412.5, 118, 604.25, 371.75],
          "confidence": 0.96875,
          "expressions": [
            {
              "name": "amusement",
              "probability": 0.6231
            },
            {
              "name": "joy",
              "probability": 0.4418
            }
          ],
          "descriptions": [
            {
              "name": "smile",
              "probability": 0.8125
            }
          ]
        },
        {
          "face_id": 1,
          "bbox": [98, 140.25, 251.5, 348.75],
          "confidence": 0.9375,
          "expressions": [
            {
              "name": "concentration",
              "probability": 0.5108
            }
          ],
          "descriptions": [
            {
              "name": "jaw drop",
              "probability": 0.2266
            }
          ]
        },
        {
          "face_id": null,
          "bbox": [702, 160.5, 790.25, 276],
          "confidence": 0.90625,
          "expressions": null,
          "descriptions": null
        }
      ]
    },
    {
      "frame_id": 1,
      "faces": [
        {
          "face_id": 0,
          "bbox": [420.75, 121.5, 611, 374.25],
          "confidence": 0.953125,
          "expressions": [
            {
              "name": "amusement",
              "probability": 0.5742
            },
            {
              "name": "joy",
              "probability": 0.4016
            }
          ],
          "descriptions": [
            {
              "name": "smile",
              "probability": 0.7695
            }
          ]
        }
      ]
    }
  ],
  "face_state": "v1.Qm9keSBvZiB0aGUgc2VhbGVkIGlkZW50aXR5IGNhY2hlLCBiYXNlNjR1cmwgd2l0aG91dCBwYWRkaW5n"
}
```

**`request_id`**

Identifies the request in Hume's records. Quote it when contacting support.

---

**`run_id`**

Identifies the [run](/expression-measurement/docs/runs) the request was recorded as. Pass it to the run endpoints to look the run up.

---

**`received.frames`**

The number of images measured.

---

**`frame_id`**

The position of the image's `file` part in the request, starting at 0.

---

**`face_id`**

Identifies the same face from image to image within the request, and across requests when you pass back `face_state`. `null` for a face that was not measured. See [Face tracking](#face-tracking).

---

**`bbox`**

The face's location in the submitted image as `[x0, y0, x1, y1]`: the top-left and bottom-right corners in pixels, measured from the top-left of the image. Coordinates are in the pixels as stored in the file: EXIF orientation is not applied.

---

**`confidence`**

The detector's confidence that the box contains a face, from 0 to 1. Never below `face.threshold`. Unrelated to the expression scores.

---

**`expressions`**

The 48 expression names are listed in [Video measurements](/expression-measurement/docs/video/measurements#expressions). `null` for a face that was not measured.

---

**`descriptions`**

Visible facial actions such as `smile` or `jaw drop`. The 27 names are listed in [Video measurements](/expression-measurement/docs/video/measurements#descriptions). `null` for a face that was not measured.

---

**`face_state`**

Present when `face.return_face_state` was set. Pass it back in a later request to continue `face_id` numbering. See [Face tracking](#face-tracking).

---

## Matching results to images

`measurements` holds one result per image, in the order the `file` parts were sent. Each result's `frame_id` is the position of its image's `file` part, starting at 0. An image with no faces still has a result, with an empty `faces` array.

## Face tracking

`face_id` links the same face across the images of one request. IDs are assigned from 0 in the order faces are first measured, largest first within an image. A face normally keeps its ID while it is measured, and can get it back after it leaves the view or is no longer among the largest faces. The server remembers up to 32 faces, so a face that returns after many others have been measured may receive a new ID. IDs do not identify a person.

IDs restart at 0 in every request. To continue numbering across requests, set `face.return_face_state` to `true`, and pass the `face_state` from the response as `face_state` in the next request's `config`.

#### config (next request)

```json
{
  "face": {
    "return_face_state": true
  },
  "face_state": "v1.Qm9keSBvZiB0aGUgc2VhbGVkIGlkZW50aXR5IGNhY2hlLCBiYXNlNjR1cmwgd2l0aG91dCBwYWRkaW5n"
}
```

1. **Treat `face_state` as opaque.** Pass it back unchanged.
2. **A state is accepted for 24 hours after it was issued.** A state that has expired, was altered, or was issued to another organization is ignored, and numbering starts again from 0.

## Configuration

To change the detection settings, send a `config` part. Fields you omit keep their defaults.

#### config

```json
{
  "face": {
    "threshold": 0.8,
    "min_size": 40
  }
}
```

**`face.threshold`** — default: 0.9

The minimum detection confidence for a face to be measured, from 0 to 1. Lower values include more faces at the risk of false detections. The scale is specific to this API, so thresholds tuned for other face detection APIs do not carry over. Reported rounded to four decimal places.

---

**`face.min_size`** — default: 60

The smallest face to measure, as the shorter side of its bounding box in pixels of the submitted image. At least 1.

---

**`face.return_face_state`** — default: false

Return a `face_state` with the response, so a later request can continue `face_id` numbering.

---

**`face_state`**

The `face_state` from an earlier response, so `face_id` continues from the faces that request saw.

---

## Errors

A failed request returns an error body with a `code`, a `message`, and a `request_id`. A `401` carries only a `message`, and a `request_id` when an API key was rejected. [HTTP errors](/expression-measurement/docs/errors#http-errors) describes the format.

<table>
  <thead>
    <tr>
      <th width="12%">
        Status
      </th>

      <th>
        Code
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        400
      </td>

      <td>
        `message_invalid`
      </td>

      <td>
        The multipart body could not be read, has no 

        `file`

         part, or has a part with another name.
      </td>
    </tr>

    <tr>
      <td>
        400
      </td>

      <td>
        `config_invalid`
      </td>

      <td>
        The 

        `config`

         part is not valid JSON, names an unrecognized field, holds a value outside its range, or appears more than once.
      </td>
    </tr>

    <tr>
      <td>
        400
      </td>

      <td>
        `image_decode_failed`
      </td>

      <td>
        An image could not be decoded.
      </td>
    </tr>

    <tr>
      <td>
        401
      </td>

      <td>
        None
      </td>

      <td>
        The request carried no valid API key or access token. A key from app.hume.ai is not valid here; see 

        [Authentication](/expression-measurement/docs/authentication#api-keys)

        .
      </td>
    </tr>

    <tr>
      <td>
        403
      </td>

      <td>
        `forbidden`
      </td>

      <td>
        Your organization is not permitted to use this endpoint. Contact support.
      </td>
    </tr>

    <tr>
      <td>
        405
      </td>

      <td>
        `method_not_allowed`
      </td>

      <td>
        The endpoint does not accept this HTTP method. The 

        `Allow`

         header lists the methods it does accept.
      </td>
    </tr>

    <tr>
      <td>
        413
      </td>

      <td>
        `image_too_large`
      </td>

      <td>
        The request body is over 25 MB, an image is over 2 MB, or an image has more than 8,294,400 pixels.
      </td>
    </tr>

    <tr>
      <td>
        415
      </td>

      <td>
        `unsupported_media_type`
      </td>

      <td>
        An image is not a JPEG.
      </td>
    </tr>

    <tr>
      <td>
        500
      </td>

      <td>
        `internal_error`
      </td>

      <td>
        The server failed while serving the request. Retry it. If the failure recurs, contact Hume with the 

        `request_id`

        .
      </td>
    </tr>

    <tr>
      <td>
        503
      </td>

      <td>
        `server_busy`
      </td>

      <td>
        The service is too busy to serve the request now. Wait the number of seconds in the 

        `Retry-After`

         header, then retry.
      </td>
    </tr>
  </tbody>
</table>

## Next steps

#### [Video measurements](/expression-measurement/docs/video/measurements)

Every expression and description the endpoint returns.

#### [Scores](/expression-measurement/docs/scores)

How to read probabilities.