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

# Runs

**A run is the record of one upload request or one realtime session.** Each measured upload request and each realtime session is recorded as a run. The run endpoints return its status, the configuration it ran under, and the problems it reported, both while a session is in progress and after it closes. Measurements come only from the upload response or the session, so store the results you need. Recording never delays a measurement, so on rare occasions a run, or part of one, is not recorded.

<table>
  <thead>
    <tr>
      <th width="48%">
        Endpoint
      </th>

      <th>
        Returns
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        [`GET /v1/expression/run`](/expression-measurement/reference/runs/list)
      </td>

      <td>
        Paginated list of your organization's runs.
      </td>
    </tr>

    <tr>
      <td>
        [`GET /v1/expression/run/{run_id}`](/expression-measurement/reference/runs/get)
      </td>

      <td>
        One run: its status and configuration.
      </td>
    </tr>

    <tr>
      <td>
        [`GET /v1/expression/run/{run_id}/events`](/expression-measurement/reference/runs/list-events)
      </td>

      <td>
        The problems the run reported, in order.
      </td>
    </tr>
  </tbody>
</table>

## Run IDs

A run's `run_id` is the identifier it was given when it started:

1. **For an upload request**, the `run_id` in the response from [Audio upload](/expression-measurement/docs/audio/upload) or [Image upload](/expression-measurement/docs/video/upload).
2. **For a realtime session**, the `session_id` in `session.created`. See [Sessions](/expression-measurement/docs/sessions).

A run can take a moment to appear after the response or `session.created` message that carries its ID.

An audio upload is recorded as a run once its audio reaches measurement, and an image upload once its images reach the checks. A request that fails after that point, including an image upload whose first image fails its check, is recorded with status `errored`. One rejected earlier has no run. Error bodies carry a `request_id` but no `run_id`; quote the `request_id` when contacting support.

Runs are scoped to your organization. A run in another organization returns `404`, the same as a run that does not exist.

## Run status

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

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

  <tbody>
    <tr>
      <td>
        `live`
      </td>

      <td>
        The run is in progress.
      </td>
    </tr>

    <tr>
      <td>
        `ended`
      </td>

      <td>
        The run completed.
      </td>
    </tr>

    <tr>
      <td>
        `errored`
      </td>

      <td>
        The run ended because of a failure, or because the connection was lost. A live run the server has not heard from in five minutes is also reported 

        `errored`

        , and reads 

        `live`

         again if the server hears from it.
      </td>
    </tr>
  </tbody>
</table>

## Listing runs

`GET /v1/expression/run` returns your organization's runs, newest first. The filters combine, so the request below returns the audio upload requests that started on October 9, 2024.

#### cURL

```bash
curl -G https://api.cloud.hume.ai/v1/expression/run \
  -H "X-Hume-Api-Key: $HUME_API_KEY" \
  -d endpoint=audio \
  -d mode=file \
  -d started_after=2024-10-09T00:00:00Z \
  -d started_before=2024-10-10T00:00:00Z
```

#### Python

```python
from datetime import datetime, timezone

from hume_expression_measurement import ExpressionMeasurementClient

client = ExpressionMeasurementClient()

page = client.runs.list(
    endpoint="audio",
    mode="file",
    started_after=datetime(2024, 10, 9, tzinfo=timezone.utc),
    started_before=datetime(2024, 10, 10, tzinfo=timezone.utc),
)

for run in page.runs:
    print(run.run_id, run.status)
```

#### Node.js

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

const client = new ExpressionMeasurementClient();

const page = await client.runs.list({
  endpoint: "audio",
  mode: "file",
  startedAfter: new Date("2024-10-09T00:00:00Z"),
  startedBefore: new Date("2024-10-10T00:00:00Z"),
});

for (const run of page.runs) {
  console.log(run.runId, run.status);
}
```

To send these requests from this page, open a panel and enter your API key.

### Request

GET [https://api.cloud.hume.ai/v1/expression/run](https://api.cloud.hume.ai/v1/expression/run)

**`runs_list_example`**

```curl runs_list_example
curl -G https://api.cloud.hume.ai/v1/expression/run \
     -H "X-Hume-Api-Key: <apiKey>" \
     -d page_size=4
```

**`runs_list_example`**

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

async function main() {
    const client = new ExpressionMeasurementClient({
        apiKey: "YOUR_API_KEY_HERE",
    });
    await client.runs.list({
        pageSize: 4,
    });
}
main();

```

**`runs_list_example`**

```python runs_list_example
from hume_expression_measurement import ExpressionMeasurementClient

client = ExpressionMeasurementClient(
    api_key="YOUR_API_KEY_HERE",
)

client.runs.list(
    page_size=4,
)

```

**`page_size`** — default: 50

How many runs to return, from 1 to 200.

---

**`cursor`**

The `next_cursor` of the previous page. See [Pagination](#pagination).

---

**`user_id`**

Only runs made by this user.

---

**`status`**

Only runs with this status: `live`, `ended`, or `errored`.

---

**`mode`**

Only runs that reached the API this way: `file` for upload requests, `realtime` for sessions.

---

**`endpoint`**

Only runs measured by this endpoint: `audio` or `video`.

---

**`started_after`**

Only runs that started at or after this time, in RFC 3339 format.

---

**`started_before`**

Only runs that started before this time, in RFC 3339 format.

---

`started_after` includes its boundary and `started_before` excludes it, so consecutive time windows that share a boundary neither skip a run nor return one twice.

#### 200 OK

```json
{
  "runs": [
    {
      "run_id": "01926f3c-d33c-7c9f-9c91-4502d80834d5",
      "user_id": "8d3f4a1e-6b2c-4f7d-9e0a-1b2c3d4e5f60",
      "status": "ended",
      "endpoint": "audio",
      "mode": "file",
      "started_at": "2024-10-09T03:05:10.204Z",
      "ended_at": "2024-10-09T03:05:11.463Z",
      "bytes_received": 160000,
      "bytes_recorded": 160000,
      "metered_seconds": 10.0
    }
  ],
  "next_cursor": "2024-10-09T03:05:10.204000Z|01926f3c-d33c-7c9f-9c91-4502d80834d5"
}
```

**`run_id`**

The run's ID. See [Run IDs](#run-ids).

---

**`user_id`**

The Hume user who made the run.

---

**`status`**

See [Run status](#run-status).

---

**`endpoint`**

`audio` or `video`.

---

**`mode`**

`file` for an upload request, `realtime` for a session.

---

**`started_at, ended_at`**

When the run started and ended. `ended_at` is `null` while the run is in progress. For a run reported `errored` because the server stopped hearing from it, `ended_at` is the last time the server heard from it.

---

**`bytes_received`**

Bytes of media the server took in. A realtime video frame counts once it passes the send-rate check, even if it is then rejected as invalid. `null` until the run ends, and for a run whose end was never recorded.

---

**`bytes_recorded`**

How far the server got in storing the run's media, in bytes. Media that could not be stored is included, so this can exceed what was kept. `null` when the run's media was not being stored, or when the server has not yet reported how much it stored, as early in a live run.

---

**`metered_seconds`**

Billable seconds for the run. For audio, the duration of the audio the server took in. For video, one third of a second for each frame or image the server took in, regardless of frame rate, including a realtime frame rejected as invalid after it passed the send-rate check. A run that took in any media is billed at least 10 seconds, applied when the run ends. Updated while a realtime session is in progress. A failed upload request is not billed, and its run reports 0. See [Billing](/expression-measurement/docs/billing).

---

## Pagination

The list endpoint and the events endpoint return one page at a time. Every page but the last carries a `next_cursor`; on the last page the field is absent. Pass it back unchanged as `cursor` to fetch the next page. A cursor the endpoint did not issue is rejected with `invalid_request`.

## Getting a run

`GET /v1/expression/run/{run_id}` returns the fields of the list, plus the configuration the run ran under and, for a realtime session, how the connection ended.

#### cURL

```bash
curl https://api.cloud.hume.ai/v1/expression/run/01926f3a-5b1c-7d2e-8f40-3a9b7c1d2e5f \
  -H "X-Hume-Api-Key: $HUME_API_KEY"
```

#### Python

```python
from hume_expression_measurement import ExpressionMeasurementClient

client = ExpressionMeasurementClient()

run = client.runs.get("01926f3a-5b1c-7d2e-8f40-3a9b7c1d2e5f")

print(run.status, run.close_reason)
```

#### Node.js

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

const client = new ExpressionMeasurementClient();

const run = await client.runs.get({
  runId: "01926f3a-5b1c-7d2e-8f40-3a9b7c1d2e5f",
});

console.log(run.status, run.closeReason);
```

### Request

GET [https://api.cloud.hume.ai/v1/expression/run/\{run_id}](https://api.cloud.hume.ai/v1/expression/run/\{run_id})

**`runs_get_example`**

```curl runs_get_example
curl https://api.cloud.hume.ai/v1/expression/run/01926f3a-5b1c-7d2e-8f40-3a9b7c1d2e5f \
     -H "X-Hume-Api-Key: <apiKey>"
```

**`runs_get_example`**

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

async function main() {
    const client = new ExpressionMeasurementClient({
        apiKey: "YOUR_API_KEY_HERE",
    });
    await client.runs.get({
        runId: "01926f3a-5b1c-7d2e-8f40-3a9b7c1d2e5f",
    });
}
main();

```

**`runs_get_example`**

```python runs_get_example
from hume_expression_measurement import ExpressionMeasurementClient

client = ExpressionMeasurementClient(
    api_key="YOUR_API_KEY_HERE",
)

client.runs.get(
    run_id="01926f3a-5b1c-7d2e-8f40-3a9b7c1d2e5f",
)

```

#### 200 OK

```json
{
  "run_id": "01926f3a-5b1c-7d2e-8f40-3a9b7c1d2e5f",
  "user_id": "8d3f4a1e-6b2c-4f7d-9e0a-1b2c3d4e5f60",
  "status": "ended",
  "endpoint": "audio",
  "mode": "realtime",
  "started_at": "2024-10-09T03:02:28.380Z",
  "ended_at": "2024-10-09T03:02:55.104Z",
  "close_code": 1000,
  "close_reason": "client_request",
  "bytes_received": 832000,
  "bytes_recorded": 832000,
  "metered_seconds": 26.0,
  "config": {
    "schema": "v4",
    "audio": {
      "type": "audio/pcm",
      "encoding": "s16le",
      "sample_rate": 16000,
      "channels": 1
    },
    "measurement_timer_ms": 3000
  }
}
```

**`close_code`**

The WebSocket close code the session ended with, or 1011 if the connection was lost without a close frame. `null` for upload requests, for sessions still in progress, and for sessions whose close was not recorded, such as when the client's close frame carried no code.

---

**`close_reason`**

Why the session ended. When the server closed it: `client_request`, `server_shutdown`, `idle_timeout`, or `error`. When the connection was lost without a close frame: `error`. When the client closed the WebSocket: the reason text from its close frame. `null` for upload requests, for sessions still in progress, and for sessions whose close was not recorded, such as when the client's close frame carried no code.

---

**`config`**

The configuration the run ran under, with every default filled in. An audio run has `audio` and `measurement_timer_ms`. A video run has `image` and `face`, with `face.threshold` and `face.min_size`.

---

**`config.schema`**

The layout of the configuration document, `v4`. If you store configurations, branch on it so a later layout is recognized rather than misread.

---

## Getting events

`GET /v1/expression/run/{run_id}/events` returns the problems a realtime session reported, in order: the frames it rejected and the failures it hit. A run with no problems has an empty log. The only event an upload request can record is `video_recording_abandoned`, on the image upload endpoint; any other upload problem fails the whole request and appears in its error response. Two kinds of problem are left out of the log: errors that answered a text message, such as `config_invalid`, and an `internal_error` on the audio endpoint, which shows as the run's `errored` status.

#### cURL

```bash
curl https://api.cloud.hume.ai/v1/expression/run/01926f3a-5b1c-7d2e-8f40-3a9b7c1d2e5f/events \
  -H "X-Hume-Api-Key: $HUME_API_KEY"
```

#### Python

```python
from hume_expression_measurement import ExpressionMeasurementClient

client = ExpressionMeasurementClient()

page = client.runs.list_events("01926f3a-5b1c-7d2e-8f40-3a9b7c1d2e5f")

for event in page.events:
    print(event.seq, event.kind)
```

#### Node.js

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

const client = new ExpressionMeasurementClient();

const page = await client.runs.listEvents({
  runId: "01926f3a-5b1c-7d2e-8f40-3a9b7c1d2e5f",
});

for (const event of page.events) {
  console.log(event.seq, event.kind);
}
```

### Request

GET [https://api.cloud.hume.ai/v1/expression/run/\{run_id}/events](https://api.cloud.hume.ai/v1/expression/run/\{run_id}/events)

**`runs_list-events_example`**

```curl runs_list-events_example
curl -G https://api.cloud.hume.ai/v1/expression/run/01926f2c-6d92-7a41-9d26-5e8b0c3f71a9/events \
     -H "X-Hume-Api-Key: <apiKey>" \
     -d page_size=2
```

**`runs_list-events_example`**

```typescript runs_list-events_example
import { ExpressionMeasurementClient } from "@humeai/expression-measurement-node";

async function main() {
    const client = new ExpressionMeasurementClient({
        apiKey: "YOUR_API_KEY_HERE",
    });
    await client.runs.listEvents({
        runId: "01926f2c-6d92-7a41-9d26-5e8b0c3f71a9",
        pageSize: 2,
    });
}
main();

```

**`runs_list-events_example`**

```python runs_list-events_example
from hume_expression_measurement import ExpressionMeasurementClient

client = ExpressionMeasurementClient(
    api_key="YOUR_API_KEY_HERE",
)

client.runs.list_events(
    run_id="01926f2c-6d92-7a41-9d26-5e8b0c3f71a9",
    page_size=2,
)

```

#### 200 OK

```json
{
  "events": [
    {
      "seq": 1,
      "at": "2024-10-09T03:02:31.720Z",
      "kind": "invalid_audio_frame",
      "detail": {
        "detail": "binary audio frame must contain complete 16-bit PCM samples"
      }
    },
    {
      "seq": 2,
      "at": "2024-10-09T03:02:40.344Z",
      "kind": "rate_limited"
    }
  ],
  "at_cap": true,
  "capped_kinds": ["rate_limited"],
  "gaps": false
}
```

The endpoint takes `page_size` (default 50, from 1 to 200) and `cursor`, and pages as described in [Pagination](#pagination).

**`events[].seq`**

The entry's position in the log.

---

**`events[].at`**

When the event was recorded.

---

**`events[].kind`**

What happened. See the table below.

---

**`events[].detail`**

Details that depend on `kind`: the reason a frame was rejected (`detail`), the size limit a message exceeded (`limit_bytes`), whether a measurement failure was temporary and how many failed in a row (`retryable`, `consecutive`), or why recording stopped (`reason`). Absent for `rate_limited`.

---

**`at_cap`**

`true` when at least one kind of event reached its logging limit for the run. Events of that kind after the limit were not logged, so the log may undercount them. `capped_kinds` names the kinds. When `gaps` is `true`, `at_cap` can be `false` even though a limit was reached.

---

**`capped_kinds`**

The kinds that reached their limit. Empty when none did.

---

**`gaps`**

`true` when entries are missing from the log: events that were numbered but never stored.

---

<table>
  <thead>
    <tr>
      <th>
        Kind
      </th>

      <th width="12%">
        Endpoint
      </th>

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

  <tbody>
    <tr>
      <td>
        `invalid_audio_frame`
      </td>

      <td>
        Audio
      </td>

      <td>
        An audio frame was rejected.
      </td>
    </tr>

    <tr>
      <td>
        `invalid_image_frame`
      </td>

      <td>
        Video
      </td>

      <td>
        An image frame was rejected, or face detection failed on a frame and the session received an 

        `internal_error`

         for it.
      </td>
    </tr>

    <tr>
      <td>
        `rate_limited`
      </td>

      <td>
        Both
      </td>

      <td>
        Media arrived faster than the send rate, and a frame was rejected.
      </td>
    </tr>

    <tr>
      <td>
        `inference_failed`
      </td>

      <td>
        Video
      </td>

      <td>
        A frame was skipped because its measurement failed. The session received an 

        `internal_error`

         for it.
      </td>
    </tr>

    <tr>
      <td>
        `message_too_large`
      </td>

      <td>
        Both
      </td>

      <td>
        A message exceeded the size limit, and the session ended.
      </td>
    </tr>

    <tr>
      <td>
        `video_recording_abandoned`
      </td>

      <td>
        Video
      </td>

      <td>
        The server stopped storing the run's frames. Results were still delivered. The server no longer records this event, so it appears only on older runs.
      </td>
    </tr>
  </tbody>
</table>

## Errors

The run endpoints return the error body described in [HTTP errors](/expression-measurement/docs/errors#http-errors). A `401` carries only a `message`, and a `request_id` when an API key was rejected.

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

      <th>
        Code
      </th>

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

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

      <td>
        `invalid_request`
      </td>

      <td>
        A query parameter has a value the endpoint does not accept, or 

        `cursor`

         is not one the endpoint issued. Sent by the list and events endpoints.
      </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>
        404
      </td>

      <td>
        `not_found`
      </td>

      <td>
        No run with this ID exists in your organization, or no endpoint exists at this path.
      </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>
        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

#### [Sessions](/expression-measurement/docs/sessions)

The realtime protocol and the `session_id` each session is recorded under.

#### [Billing](/expression-measurement/docs/billing)

How each endpoint counts the usage in `metered_seconds`.