---
title: "Reading recordings"
description: "List and filter recordings, read their capture data and evidence text, get frames and transcripts, file issues, and embed share links with oEmbed."
canonical: https://faylee.app/docs/api/reading
---

# Reading recordings

These endpoints read and change recordings in the token owner's workspace. Every recording endpoint takes either the numeric `id` or the `share_id` from a share link as `:id`. A recording outside the workspace, or in the trash, gets `404`.

## List recordings

`GET /recordings` lists the workspace's ready recordings, newest first, 50 a page.

| Parameter | Notes |
| --- | --- |
| `q` | Matches the title, description or page URL, case-insensitive |
| `kind` | `video`, `replay` or `screenshot` |
| `folder` | A folder id from [`GET /folders`](https://faylee.app/docs/api/uploading#choose-a-folder) |
| `tag` | One tag |
| `author` | A member's numeric user id |
| `problems` | `true` for recordings with at least one error or failed request |
| `status` | Triage status: `open`, `in_progress`, `resolved` or `wont_fix` |
| `priority` | `urgent`, `high`, `medium` or `low` |
| `assignee` | The assigned member's numeric user id |
| `since` | An ISO 8601 time. Only recordings created at or after it |
| `page` | Page number, from 1 |

Unknown values for `kind`, `status` and `priority` are ignored rather than refused.

```bash
curl -G https://app.faylee.app/api/v1/recordings \
  -H "Authorization: Bearer $FAYLEE_TOKEN" \
  --data-urlencode "problems=true" \
  --data-urlencode "since=2026-09-29T00:00:00Z"
```

```json
{
  "recordings": [
    {
      "id": 4812,
      "share_id": "V1StGXR8_Z",
      "kind": "video",
      "title": "Checkout fails with a 500",
      "status": "ready",
      "error_count": 2,
      "failed_request_count": 1,
      "share_url": "https://app.faylee.app/s/V1StGXR8_Z",
      "author": "Ann Park"
    }
  ],
  "page": 1,
  "total_pages": 3
}
```

Each item is a full [recording object](https://faylee.app/docs/api/overview#the-recording-object). The example above is shortened. A `since` that is not an ISO 8601 time gets `422` with `since must be an ISO 8601 time`.

To poll for new recordings, pass the `created_at` of the newest one you have seen as `since`. To be told instead, subscribe to the `recording.ready` [webhook](https://faylee.app/docs/api/webhooks.md).

## Get a recording

`GET /recordings/:id` returns the [recording object](https://faylee.app/docs/api/overview#the-recording-object). Unlike the list, it also returns recordings that are still `uploading` or `processing`, or that `failed`.

```bash
curl https://app.faylee.app/api/v1/recordings/V1StGXR8_Z \
  -H "Authorization: Bearer $FAYLEE_TOKEN"
```

## Update a recording

`PATCH /recordings/:id` changes the title, description or who can open the recording, and returns the recording object.

| Field | Notes |
| --- | --- |
| `recording.title` | |
| `recording.description` | |
| `recording.visibility` | `workspace` or `public`. See below for `password` |

```bash
curl -X PATCH https://app.faylee.app/api/v1/recordings/V1StGXR8_Z \
  -H "Authorization: Bearer $FAYLEE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "recording": { "visibility": "public" } }'
```

The API cannot set a share password. Setting `password` works only on a recording that already has a password from the app. Otherwise it gets `422` with `Share password is needed for a password-protected link`. A change of visibility is written to the workspace's audit log, as it is in the app.

## Trash a recording

`DELETE /recordings/:id` moves the recording to the trash and returns `204`. Only the recording's author or a workspace admin can do this. Anyone else gets `403`.

The recording is deleted for good after 30 days, unless someone restores it from the trash in the app.

## Capture data

`GET /recordings/:id/capture` returns the recording's capture bundle, `data.json`, as it was uploaded. Its format is described in [Capture format](https://faylee.app/docs/api/capture-format.md).

```bash
curl https://app.faylee.app/api/v1/recordings/V1StGXR8_Z/capture \
  -H "Authorization: Bearer $FAYLEE_TOKEN" -o data.json
```

### Evidence text

With `format=evidence`, the same endpoint returns `text/plain`: the capture written out for a language model to read. It starts with the kind, duration, title and description, the environment and any metadata the app attached. Then comes a timeline with one line per event, each starting with its time in milliseconds since the recording started:

```bash
curl "https://app.faylee.app/api/v1/recordings/V1StGXR8_Z/capture?format=evidence" \
  -H "Authorization: Bearer $FAYLEE_TOKEN"
```

```bash
[t=4900ms] user clicked button "Pay now"
```

The timeline covers pages, actions, console messages, errors, requests, socket messages, page edits, the reporter's marks and Web Vitals. Long values are shortened. When the text would pass 200,000 characters, the least telling events (plain logs, successful requests, measurements) are left out first, and a last line says how many. Stack frames that Faylee resolved through the site's source maps point at the original source.

Both forms return `422` when the recording's storage is no longer connected. The evidence form also returns `422` for a capture made with an older format version, or a damaged one.

:::tip
The evidence text is what the Faylee assistant reads. It suits other agents too, and it is much smaller than the raw bundle.
:::

## Frames

`GET /recordings/:id/frames` returns still images from a video or a replay.

| Parameter | Notes |
| --- | --- |
| `mode` | `overview` (default), `at` or `range` |
| `at` | For `mode=at`: comma-separated times in milliseconds, up to 20 |
| `from`, `to` | For `mode=range`: the window in milliseconds. Defaults to the whole recording |
| `count` | For `mode=range`: how many frames to spread across the window, 1 to 20, 5 by default |
| `size` | `small`, `medium` or `large`: 360, 720 or 1080 pixels high. `medium` by default |

`overview` returns one image of 6 to 16 evenly spaced frames in a grid, each labelled with its time. Longer recordings get more frames.

```json
{
  "mode": "overview",
  "size": "medium",
  "columns": 4,
  "rows": 2,
  "t_ms": [3000, 9000, 15000, 21000, 27000, 33000, 39000, 45000],
  "url": "https://storage.example.com/recordings/V1StGXR8_Z/frames/..."
}
```

`at` and `range` return one image per time. Times outside the recording are moved to its start or end.

```bash
curl "https://app.faylee.app/api/v1/recordings/V1StGXR8_Z/frames?mode=at&at=5000,12000&size=small" \
  -H "Authorization: Bearer $FAYLEE_TOKEN"
```

```json
{
  "mode": "at",
  "size": "small",
  "frames": [
    { "t_ms": 5000, "url": "https://storage.example.com/..." },
    { "t_ms": 12000, "url": "https://storage.example.com/..." }
  ]
}
```

A screenshot returns its own image as a single frame at `t_ms` 0. A replay's frames are drawn on the server by rebuilding the captured page at each moment. Images, fonts and stylesheets the recorded site loaded from its own hosts are missing from them.

URLs are presigned and expire after one hour. Frames are made once and kept, so asking again for the same times is fast. The endpoint returns `422`, with a message pointing at the steps, console and network instead, when there is nothing to draw: a replay with no captured page, or a replay that takes too long to draw.

## Transcript

`GET /recordings/:id/transcript` returns what the reporter said while recording a video, as timed segments.

```json
{
  "status": "ready",
  "language": "english",
  "segments": [
    { "startMs": 100, "endMs": 900, "text": "Here it breaks", "words": [{ "text": "Here", "startMs": 100 }] }
  ]
}
```

`status` is `pending`, `ready` or `failed`, or `null` when there is no transcript. `segments` is empty until the transcript is ready, and when the recording has no narration. Times are on the recording's clock. `words` may be absent.

## Attachments

`GET /recordings/:id/attachments` lists the files added to a recording, oldest first:

```json
{
  "attachments": [
    { "id": 31, "filename": "server.log", "content_type": "text/plain", "byte_size": 48213, "url": "https://storage.example.com/..." }
  ]
}
```

`url` is a presigned download link that expires after one hour. A screenshot shared with marks drawn on it has an attachment named `screenshot-without-marks.png`: the same image without the marks, with blurred areas kept. The marks themselves are in the capture's [`annotations`](https://faylee.app/docs/api/capture-format#annotation).

`POST /recordings/:id/attachments` adds one. Send its metadata, then `PUT` the bytes to the returned `upload_url` with the same `Content-Type`.

| Field | Notes |
| --- | --- |
| `attachment.filename` | Required, up to 200 characters |
| `attachment.content_type` | Defaults to `application/octet-stream` |
| `attachment.byte_size` | Greater than 0, at most 25 MB |

```bash
curl -X POST https://app.faylee.app/api/v1/recordings/V1StGXR8_Z/attachments \
  -H "Authorization: Bearer $FAYLEE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "attachment": { "filename": "server.log", "content_type": "text/plain", "byte_size": 48213 } }'
```

```json
{ "id": 31, "filename": "server.log", "upload_url": "https://storage.example.com/...?X-Amz-Signature=..." }
```

```bash
curl -X PUT "$UPLOAD_URL" -H "Content-Type: text/plain" --data-binary @server.log
```

A recording can have 10 attachments. The eleventh gets `422`.

## Email a recording

`POST /recordings/:id/emails` emails the recording's link to up to 10 people, from the token's owner.

| Field | Notes |
| --- | --- |
| `emails` | Email addresses. Duplicates and blanks are dropped |
| `note` | Optional message included in the email |

```bash
curl -X POST https://app.faylee.app/api/v1/recordings/V1StGXR8_Z/emails \
  -H "Authorization: Bearer $FAYLEE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "emails": ["dana@example.com"], "note": "This is the checkout bug from standup" }'
```

`202` returns `{ "sent_to": ["dana@example.com"] }`. Addresses are lowercased. An empty list, more than 10 addresses, or an invalid address gets `422`.

## Issues

File an issue in one of the workspace's connected issue trackers. See [Issue trackers](https://faylee.app/docs/integrations/issue-trackers.md) for connecting one.

### List trackers

`GET /integrations` lists the trackers you can file to:

```json
{
  "trackers": [{ "id": 3, "provider": "linear", "name": "Linear" }],
  "recent": [],
  "available": [{ "provider": "github", "name": "GitHub" }],
  "connect_url": "https://app.faylee.app/settings/integrations",
  "can_connect": false
}
```

| Field | Notes |
| --- | --- |
| `trackers` | Connected trackers that are ready to use. File with their `id` |
| `recent` | Where the token's owner filed last, with the fields they used |
| `available` | Trackers the workspace has not connected |
| `connect_url` | Where an admin connects one |
| `can_connect` | Whether the token's owner is an admin |

### Tracker fields

`GET /integrations/:id/fields` returns the choices a tracker offers, by field. Which fields appear depends on the tracker. A field's values are `{ value, label }` pairs.

```json
{
  "assignee": [{ "value": "u_81", "label": "Dana Ruiz" }],
  "labels": [{ "value": "bug", "label": "bug" }],
  "priority": [{ "value": "1", "label": "Urgent" }]
}
```

The response is `{}` when the tracker could not be reached.

### File an issue

`POST /recordings/:id/issues`:

| Field | Notes |
| --- | --- |
| `integration_id` | Required. A tracker `id` from `GET /integrations` |
| `issue.title` | The issue title |
| `issue.body` | Markdown |
| `issue.fields.assignee` | Optional. A `value` from the tracker's fields |
| `issue.fields.priority` | Optional |
| `issue.fields.labels` | Optional. A list of values |

```bash
curl -X POST https://app.faylee.app/api/v1/recordings/V1StGXR8_Z/issues \
  -H "Authorization: Bearer $FAYLEE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "integration_id": 3,
    "issue": {
      "title": "Checkout fails with a 500",
      "body": "Paying with a saved card returns a 500 from /api/orders.",
      "fields": { "labels": ["bug"] }
    }
  }'
```

`201` means the issue was created:

```json
{ "id": 58, "status": "created", "key": "WEB-142", "url": "https://linear.app/acme/issue/WEB-142" }
```

Faylee adds a line with the recording's share link to the end of the body.

`202` with `"status": "pending"` means the tracker was down. Faylee keeps retrying, and `key` and `url` are `null` until it succeeds. When the tracker refuses the issue, the response is `422` with the tracker's reason, and nothing is kept. The `issue.created` [webhook](https://faylee.app/docs/api/webhooks.md) is sent once the tracker has created the issue.

## oEmbed

`GET /oembed` returns an embeddable player for a share link. It lives at the site root, not under `/api/v1`, and takes no token.

| Parameter | Notes |
| --- | --- |
| `url` | Required. A share or embed link, such as `https://app.faylee.app/s/V1StGXR8_Z` |
| `maxwidth` | Optional. At most 960 |
| `maxheight` | Optional. At most 540 |
| `format` | Optional. Only `json` is supported |

```bash
curl -G https://app.faylee.app/oembed --data-urlencode "url=https://app.faylee.app/s/V1StGXR8_Z"
```

```json
{
  "version": "1.0",
  "type": "rich",
  "provider_name": "Faylee",
  "provider_url": "https://app.faylee.app/",
  "title": "Checkout fails with a 500",
  "author_name": "Ann Park",
  "width": 960,
  "height": 540,
  "html": "<iframe src=\"https://app.faylee.app/embed/V1StGXR8_Z\" width=\"960\" height=\"540\" frameborder=\"0\" allow=\"fullscreen\" allowfullscreen></iframe>",
  "thumbnail_url": "https://app.faylee.app/s/V1StGXR8_Z/thumbnail",
  "thumbnail_width": 640
}
```

The player keeps a 16:9 shape inside the size you allow. `thumbnail_url` and `thumbnail_width` are present only when the recording has a thumbnail.

Only public recordings can be embedded. A recording that is not public, or whose share link has expired, gets `401`. A link to another site or an unknown recording gets `404`, and a `format` other than `json` gets `501`. Links on a workspace's verified link domain work too.

Share pages advertise this endpoint with a `<link rel="alternate" type="application/json+oembed">` tag, so tools that support oEmbed discovery embed pasted share links on their own.
