Skip to content

An upload has four steps. You create the recording, upload the media in parts, upload the capture data, and tell Faylee the upload is complete. The bytes go from your client straight to the storage bucket through presigned URLs. They never pass through the API.

StepRequestSent to
1POST /recordingsFaylee
2PUT each media partStorage, through part_urls
3PUT the capture dataStorage, through data_upload_url
4POST /recordings/:id/completeFaylee

A replay has no media, so it skips step 2. Every recording, including screenshots and replays, uploads capture data in step 3.

POST /recordings with a recording object.

FieldTypeRequiredNotes
kindstringyesvideo, replay or screenshot
titlestringno
descriptionstringno
page_urlstringnoThe page the capture starts on
duration_msintegernoLength of the recording. For a trimmed video, the length after the trim
error_countintegernoShown in the library and used by the problems filter
failed_request_countintegernoShown in the library and used by the problems filter
media.content_typestringfor video and screenshotvideo/webm, video/mp4, image/png or image/jpeg
media.byte_sizeintegerfor video and screenshotGreater than 0, at most 2 GB
environmentobjectnoThe browser environment. See below
folder_idintegernoA folder from GET /folders
trim.start_ms, trim.end_msintegernoVideos only. See Trims and blurred areas
blur_regionsobject[]noVideos only. See Trims and blurred areas

environment accepts the same keys as the capture format’s Environment: userAgent, language, languages, timezone, colorScheme, cpuCores, memoryGb, online, cookiesEnabled, browser (name, version), os (name, version), viewport (width, height), screen (width, height, pixelRatio) and network (effectiveType, downlinkMbps, rttMs). Other keys are dropped.

Terminal window
curl -X POST https://app.faylee.app/api/v1/recordings \
-H "Authorization: Bearer $FAYLEE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"recording": {
"kind": "video",
"title": "Checkout fails",
"page_url": "https://shop.example.com/checkout",
"duration_ms": 42000,
"error_count": 2,
"media": { "content_type": "video/webm", "byte_size": 23068672 },
"environment": { "browser": { "name": "Chrome", "version": "131.0" } }
}
}'

201 returns the upload plan:

{
"id": 4812,
"share_url": "https://app.faylee.app/s/V1StGXR8_Z",
"data_upload_url": "https://storage.example.com/recordings/V1StGXR8_Z/data.json?X-Amz-Signature=...",
"media_upload": {
"upload_id": "2~kQ7v...",
"part_size": 10485760,
"part_urls": [
"https://storage.example.com/recordings/V1StGXR8_Z/media.webm?partNumber=1&uploadId=...",
"https://storage.example.com/recordings/V1StGXR8_Z/media.webm?partNumber=2&uploadId=...",
"https://storage.example.com/recordings/V1StGXR8_Z/media.webm?partNumber=3&uploadId=..."
]
}
}
FieldNotes
idThe recording’s numeric id. Use it to complete the upload
share_urlWhere the recording will be once it is ready
data_upload_urlPresigned PUT URL for the capture data
media_uploadnull for a replay
media_upload.upload_idThe storage multipart upload
media_upload.part_sizeBytes per part, 10 MB. The last part may be smaller
media_upload.part_urlsOne presigned PUT URL per part, in order. There are ceil(byte_size / part_size) of them

The recording now exists with status uploading. It does not appear in the library or in GET /recordings until it is ready.

GET /folders lists the workspace’s folders, sorted by name:

{ "folders": [{ "id": 12, "name": "Checkout" }, { "id": 7, "name": "Onboarding" }] }

Pass one as recording.folder_id. A folder from another workspace gets 404.

Split the file into part_size chunks. PUT chunk n to part_urls[n - 1] with the media’s Content-Type, and keep the ETag header from each response.

Terminal window
split -b 10485760 -d -a 1 checkout.webm part-
curl -X PUT "$PART_URL_1" -H "Content-Type: video/webm" --data-binary @part-0 -D - -o /dev/null | grep -i '^etag'
# ETag: "a1b2c3d4e5f6..."

Repeat for every part. Keep each ETag exactly as returned, including the quotes.

If you upload from a browser, the bucket must expose the ETag header through CORS. Without it the browser cannot read the header and you cannot complete the upload. See Storage if the workspace uses its own bucket.

PUT the capture bundle to data_upload_url with Content-Type: application/json. The bundle is described in Capture format.

Terminal window
curl -X PUT "$DATA_UPLOAD_URL" -H "Content-Type: application/json" --data-binary @data.json

Faylee does not redact this file. Hide secrets in it before you upload. See Redaction.

POST /recordings/:id/complete with the parts in order. :id must be the numeric id from the upload plan, and only the person who created the recording can complete it.

FieldTypeNotes
partsobject[]One per media part. Empty for a replay
parts[].part_numberintegerStarting at 1
parts[].etagstringThe ETag storage returned for that part
Terminal window
curl -X POST https://app.faylee.app/api/v1/recordings/4812/complete \
-H "Authorization: Bearer $FAYLEE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"parts": [
{ "part_number": 1, "etag": "\"a1b2c3d4e5f6...\"" },
{ "part_number": 2, "etag": "\"0f9e8d7c6b5a...\"" },
{ "part_number": 3, "etag": "\"99aa88bb77cc...\"" }
]
}'

200 returns the recording object with status ready, or processing when the video has a trim or blurred areas to apply.

For a replay, send an empty list:

Terminal window
curl -X POST https://app.faylee.app/api/v1/recordings/4813/complete \
-H "Authorization: Bearer $FAYLEE_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "parts": [] }'

Once the recording is ready, Faylee makes a thumbnail for media, sends the recording.ready webhook, notifies connected integrations, and, when the workspace has the assistant on, analyzes it.

Creating a recording returns 422 when a field is invalid, with the validation message:

MessageCause
Kind is not included in the listkind is missing or unknown
Media content type is not included in the listThe media type is not one of the four accepted types
Media byte size is not a numbermedia.byte_size is missing
Media byte size must be greater than 0media.byte_size is 0
Media byte size must be less than or equal to 2147483648The file is larger than 2 GB
Trim end ms must be greater than ...trim.end_ms is not after trim.start_ms
This workspace has no storage configured yetThe workspace has no bucket to upload to

Completing an upload returns 422 with one of these messages:

MessageCause
This upload was already finishedThe recording is no longer uploading. It was completed already, or it expired
The capture data never reached storageNothing was uploaded to data_upload_url
No media parts were uploadedparts is empty for a video or screenshot
Storage refused the upload: ...Storage rejected the parts, for example a wrong or missing ETag. The recording becomes failed

After a Storage refused the upload error the recording cannot be completed. Start again from step 1.

A video can ask Faylee to cut it or blur parts of it before anyone can watch it. Faylee re-encodes the stored video, so the removed parts are gone rather than hidden.

FieldNotes
trim.start_msWhere the kept part starts, in milliseconds into the uploaded video. 0 or more
trim.end_msWhere the kept part ends. Greater than start_ms
blur_regionsAreas to blur for the whole video, each { x, y, width, height } as fractions of the frame from the top left, between 0 and 1. Malformed areas are dropped

A trim needs both values. Upload the full video as recorded, with media.byte_size of the full file. The capture data is not trimmed by the server: upload a bundle already cut to the same range, with its times starting at 0, and set duration_ms to the trimmed length.

{
"recording": {
"kind": "video",
"duration_ms": 15000,
"media": { "content_type": "video/mp4", "byte_size": 31457280 },
"trim": { "start_ms": 12000, "end_ms": 27000 },
"blur_regions": [{ "x": 0.7, "y": 0.05, "width": 0.25, "height": 0.1 }]
}
}
StatusMeaning
uploadingCreated in step 1 and waiting for completion
processingCompleted, and the video is being trimmed or blurred. Nobody can watch it yet
readyVisible in the library and to the API
failedStorage refused the upload, the video could not be edited, or the upload was abandoned

A recording left in uploading for 24 hours is abandoned: its multipart upload is aborted and it becomes failed. A video that cannot be trimmed or blurred becomes failed rather than being shown uncut.

GET /recordings lists only ready recordings. To follow one upload, fetch it with GET /recordings/:id, or subscribe to the recording.ready webhook.