homeaboutvideotry it nowpricingdocs
Docs

Getting Started

Manage files and videos

List, search, update and delete your own files, folders, videos and labels from your server, for example to clean up files when a record in your database is deleted. Every request is limited to your app, so one app can never see or change another's files.

These endpoints need your private key, so call them from your server only. A request that only carries the public key, or a token limited to uploads or reads, is answered with
403 private_key_required
. The shared demo app cannot be managed. The Node, Ruby, Python and PHP libraries sign requests for you when you give them a private key.

List and search files

Filter by label, type prefix, name, hash, metadata and creation date, sort, and page with the cursor in

nextCursor
. Paging is stable: deleting files while you page never makes you skip one. See the Files reference for every parameter.

// one page, newest first. Needs the private key.
const page = await fileboy.listFiles({
  mime: "image/",
  metadata: { orderId: "42" },
  limit: 25,
})
page.data.forEach(file => console.log(file.id, file.name, file.labels))

// the next page
if (page.nextCursor) {
  await fileboy.listFiles({ mime: "image/", metadata: { orderId: "42" }, cursor: page.nextCursor })
}

Update a file

Only the fields you send change. Metadata is merged: keys you send are set, a null value removes a key, other keys stay. Ask for

replaceMetadata
to swap everything instead. A label points at one file, so adding a label that is on another file moves it.
sharePage: true
turns on the file's share page.

// metadata is merged: a null value removes a key
const updated = await fileboy.updateFileData(file.id, {
  name: "invoice-1042-final.pdf",
  metadata: { status: "paid", draft: null },
  labels: { add: ["latest-invoice"] },
})

Delete files

Deleting removes the stored data, including cached resized copies of an image, and sends a

file.deleted
webhook. A single delete of a file that is not there is a 404 (
file_not_found
), so treat 404 as already done. Bulk delete takes up to 100 ids, never fails on unknown ids and can be repeated safely.

import { notFoundError } from "@cd2/fileboy-node"

try {
  await fileboy.destroyFile(file.id)
} catch (e) {
  if (!notFoundError.is(e)) throw e // already gone is fine
}
// up to 100 ids. Unknown ids are reported, not thrown.
const { deleted, notFound, failed } = await fileboy.destroyFiles(ids)
If storage cannot be reached, a single delete answers 500 and a bulk delete lists the file in
failed
. The file is left exactly as it was, so send the request again.

A photo's EXIF data

GET /files/:id/exif
returns the camera, lens, exposure, date taken, GPS position and the rest of a photo's EXIF, IPTC and XMP metadata, as typed fields and raw tags. It holds personal data, so it is only ever served to your private key. Add
?personal=false
to leave the GPS position, place names and serial numbers out. See EXIF data.

// needs the private key: the answer can hold the GPS position and serial numbers
const exif = await fileboy.getFileExif(file.id)
exif.fields.model // "iPhone 15 Pro"
exif.fields.shutterSpeed // "1/60"
exif.fields.dateTaken // "2026-02-19T14:22:37.146-08:00"
exif.fields.gps // { latitude: 51.5007, longitude: -0.1246, altitude: 13.21 } or null

// without GPS, place names, serial numbers or the owner, to show your own users
const shareable = await fileboy.getFileExif(file.id, { personal: false })

Folders

Create folders (one by name, or a whole path at once), list them, rename and move them, and move up to 100 files at a time with

POST /files/move
. File records carry
folderId
and
folderPath
, and
GET /files
filters by folder. A folder is never part of a URL, so none of this changes a link. See Folders for the rules, including what deleting a folder does to what is inside.

// mkdir -p: makes any folder on the path that is missing
const shoes = await fileboy.createFolder({ path: "/Products/Shoes" })

await fileboy.uploadFileFromPath("./red.png", { folderPath: "/Products/Shoes" })
const page = await fileboy.listFiles({ folderId: shoes.id }) // or { folderPath: "/Products", recursive: true }

await fileboy.moveFiles([file.id], { folderId: shoes.id }) // { folderId: null } is the top level
await fileboy.updateFolder(shoes.id, { name: "Trainers" }) // links to the files don't change
await fileboy.destroyFolder(shoes.id, { contents: "moveToParent" })

Videos and labels

Deleting a video removes the original, the posters, the transcripts and every rendition, and sends

video.deleted
.

const { data: videos } = await fileboy.listVideos({ status: "ready", limit: 20 })

await fileboy.updateVideo(videos[0].id, { name: "Launch day" })
await fileboy.destroyVideo(videos[0].id) // original, renditions, posters and transcripts
const labels = await fileboy.listLabels()
await fileboy.createLabel("hero", { fileId: file.id })
await fileboy.destroyLabel("hero") // the file is kept

Video description, tags and metadata

Every video has a description (plain text, up to 5,000 characters), tags (up to 50, each up to 64 characters, no commas, case-sensitive) and metadata: your own key-value strings, like a file's, for matching the video to your records. Up to 100 keys of up to 128 characters (no

[
or
]
), values up to 4,096 characters, 16 KB in all. Set them on upload, in the dashboard or with
PATCH /videos/:id
, and find videos by them with
GET /videos?tags=launch&metadata[orderId]=42
.

The description is public: it shows on the watch page, in link previews and in

/videos/:id/data
. Tags and metadata are private. They are only in management responses, the dashboard and every
video.*
webhook, so you can match
video.ready
to your own record without storing the video id first.

// The description shows on the watch page. Tags and metadata are private.
const video = await fileboy.updateVideo(videoId, {
  description: "Our launch, in two minutes.",
  tags: { add: ["launch"], remove: ["draft"] }, // or a list to replace them
  metadata: { orderId: "42", draft: null }, // merged, null removes a key
})

// Videos with every one of the tags and all of the pairs
const { data } = await fileboy.listVideos({ tags: ["launch"], metadata: { orderId: "42" } })
Tags sent as a list replace the video's tags, and
{ "add": [...], "remove": [...] }
changes only those, so two requests at once don't undo each other. Metadata is merged like a file's. A request that would go over a limit changes nothing.

Usage

How many files and videos you have and how many bytes they store, next to your plan's limits. Data transfer is not reported.

const usage = await fileboy.getUsage()
console.log(usage.files, usage.storageBytes, usage.limits.storageBytes)

To be told before you reach a limit instead of polling, use usage alerts: an email, a dashboard banner and a

usage.threshold_reached
webhook, with thresholds you can read and change through
/usage/alerts
.

Storage breakdown

What your storage is made of: images, other files, video originals, renditions, posters, captions and the cached copies Fileboy makes of resized images. Each stored file counts once, under one kind, so the kinds add up to the same

storageBytes
as
/usage
, the number your plan's storage limit is measured against. It also lists your biggest files and videos and one total a day, so you can see growth. The dashboard shows the same on each app's Storage page. See how storage is counted.

const storage = await fileboy.getStorageUsage({ days: 90 })
console.log(storage.kinds.videoRenditions.bytes, storage.largestFiles[0]?.name)
for (const day of storage.history) console.log(day.date, day.storageBytes)

List videos

Your app's videos, newest first by default. Needs a token signed with your private key.

GET
1
https://api.fileboy.io/videos

Query Params

limit number, optional, default is 50

1 to 100.

cursor string, optional

The previous nextCursor.

sort createdAt | name, optional

What to sort by.

order asc | desc, optional

Sort direction.

name string, optional

Name contains this text.

status pending | processing | ready | failed, optional

Only videos in this state.

tags string, optional

Only videos with every one of these tags, comma-separated (tags=launch,2026) or repeated. Up to 20.

metadata[key] string, optional

Only videos whose metadata has this value for the key. Repeat for more pairs (up to 20), or send metadata={"orderId":"42"}.

createdBefore string, optional

ISO 8601.

createdAfter string, optional

ISO 8601.

Example

REQUEST
1
curl -G 'https://api.fileboy.io/videos' -H 'Authorization: token 0000000....' --data-urlencode 'status=ready' --data-urlencode 'tags=launch' --data-urlencode 'metadata[orderId]=42'
RESPONSE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{ "data": [{
  "id": "9b1c4d1e-5c1e-4b8e-9d57-3f0a4f1a7c10",
  "name": "launch.mp4",
  "createdAt": "2026-01-02T03:04:05Z",
  "status": "ready",
  "statusMessage": "",
  "duration": 12.5,
  "width": 1280,
  "height": 720,
  "playCount": 3,
  "lastPlayedAt": null,
  "description": "Our launch, in two minutes.",
  "tags": ["launch", "2026"],
  "metadata": { "orderId": "42" },
  "urls": {
    "manifest": "https://api.fileboy.io/videos/9b1c.../manifest",
    "hls": "https://api.fileboy.io/videos/9b1c.../manifest?type=hls",
    "dash": "https://api.fileboy.io/videos/9b1c.../manifest?type=dash",
    "poster": "https://api.fileboy.io/videos/9b1c.../poster"
  },
  "playerBranding": {}
}], "nextCursor": null }

Update a video

Changes the video's name, description, tags, metadata or its own player branding. Only the fields you send change, and a request that would go over a limit changes nothing. Needs your private key.

PATCH
1
https://api.fileboy.io/videos/:id

URL Params

id string

The video's id.

Body

name string, optional

The new name, 1 to 1024 characters.

description string | null, optional

Plain text up to 5000 characters, trimmed, line breaks kept. Public. "" or null removes it.

tags string[] | null | { add, remove }, optional

A list replaces the tags, null removes them all, { add: [...], remove: [...] } changes some.

metadata object, optional

String values to set, merged into the existing metadata. A null value removes a key.

replaceMetadata boolean, optional

Make metadata the complete new metadata.

playerBranding object | null, optional

This video's player branding, on top of the app's (see Player branding below). Each field it sets replaces the app's; controls merge one by one. null makes the video follow the app again.

Example

REQUEST
1
curl -X PATCH 'https://api.fileboy.io/videos/9b1c4d1e-5c1e-4b8e-9d57-3f0a4f1a7c10' -H 'Authorization: token 0000000....' -H 'Content-Type: application/json' --data '{"description":"Our launch, in two minutes.","tags":{"add":["launch"]},"metadata":{"orderId":"42"}}'
RESPONSE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{ "data": {
  "id": "9b1c4d1e-5c1e-4b8e-9d57-3f0a4f1a7c10",
  "name": "launch.mp4",
  "createdAt": "2026-01-02T03:04:05Z",
  "status": "ready",
  "statusMessage": "",
  "duration": 12.5,
  "width": 1280,
  "height": 720,
  "playCount": 3,
  "lastPlayedAt": null,
  "description": "Our launch, in two minutes.",
  "tags": ["launch", "2026"],
  "metadata": { "orderId": "42" },
  "urls": {
    "manifest": "https://api.fileboy.io/videos/9b1c.../manifest",
    "hls": "https://api.fileboy.io/videos/9b1c.../manifest?type=hls",
    "dash": "https://api.fileboy.io/videos/9b1c.../manifest?type=dash",
    "poster": "https://api.fileboy.io/videos/9b1c.../poster"
  },
  "playerBranding": {}
} }

Delete a video

Removes the original, posters, transcripts and renditions from storage, then the video, and sends video.deleted. 404 if there is no such video. If storage cannot be reached the answer is 500: the video is still listed with whatever was not yet removed, and you can repeat the request to finish. Needs your private key.

DELETE
1
https://api.fileboy.io/videos/:id

URL Params

id string

The video's id.

Example

REQUEST
1
curl -X DELETE 'https://api.fileboy.io/videos/9b1c4d1e-5c1e-4b8e-9d57-3f0a4f1a7c10' -H 'Authorization: token 0000000....'
RESPONSE
1
// 204 No Content

Player branding

The player branding every video of the app starts with: colours, a logo, which controls show, and autoplay, muted and loop. Players read the result for one video from GET /videos/:id/player-config (see Play a video). Send playerBranding alone or with requireSignedReads and stripLocationOnUpload; null clears it. A bad value is a 400 validation_error that lists every problem, and the logo must be an image in this app. GET /app/settings returns it too. Needs your private key.

PATCH
1
https://api.fileboy.io/app/settings

Body

playerBranding.accentColor, controlsColor, backgroundColor string, optional

Hex colours (#rgb or #rrggbb): the progress bar and hovered buttons, icons and text, and the area around the video.

playerBranding.logo object | null, optional

fileId (an image in this app), position (top-left, top-right, bottom-left or bottom-right; top-right by default), link (http or https, opens in a new tab) and alt (the name screen readers read, up to 200 characters).

playerBranding.controls object, optional

true or false for play, seek, volume, time, captions, speed, quality, pip, fullscreen, download and title. Every control shows by default except download, which offers the original file.

playerBranding.autoplay, muted, loop boolean, optional

Playback defaults. Autoplay always starts muted.

Example

REQUEST
1
2
3
4
5
6
7
curl -X PATCH 'https://api.fileboy.io/app/settings' -H 'Authorization: token 0000000....' -H 'Content-Type: application/json' --data '{
  "playerBranding": {
    "accentColor": "#ff3e6c",
    "logo": { "fileId": "9d44134a-0fac-4c6f-86bc-18780e8e0a45", "position": "top-right", "link": "https://example.com", "alt": "Acme" },
    "controls": { "pip": false, "download": true }
  }
}'
RESPONSE
1
2
3
4
5
6
7
8
9
10
11
{
  "data": {
    "requireSignedReads": false,
    "stripLocationOnUpload": false,
    "playerBranding": {
      "accentColor": "#ff3e6c",
      "logo": { "fileId": "9d44134a-0fac-4c6f-86bc-18780e8e0a45", "position": "top-right", "link": "https://example.com", "alt": "Acme" },
      "controls": { "pip": false, "download": true }
    }
  }
}

List labels

Your app's labels. A label is a name that points at one file, so PUBLIC_KEY+LABEL works anywhere a file id does. Needs your private key.

GET
1
https://api.fileboy.io/labels

Example

REQUEST
1
curl 'https://api.fileboy.io/labels' -H 'Authorization: token 0000000....'
RESPONSE
1
{ "data": [{ "id": "…", "name": "logo", "createdAt": "2026-01-02T03:04:05Z", "fileId": "9d44134a-0fac-4c6f-86bc-18780e8e0a45" }] }

Create a label

Label names are 1 to 100 characters with no spaces and none of - + / \ ? # % &. Answers 409 label_taken if the name exists. Needs your private key.

POST
1
https://api.fileboy.io/labels

Body

name string

The label.

fileId string, optional

A file of yours to point it at.

Example

REQUEST
1
curl -X POST 'https://api.fileboy.io/labels' -H 'Authorization: token 0000000....' -H 'Content-Type: application/json' --data '{"name":"hero","fileId":"9d44134a-0fac-4c6f-86bc-18780e8e0a45"}'
RESPONSE
1
{ "data": { "id": "…", "name": "hero", "createdAt": "2026-01-02T03:04:05Z", "fileId": "9d44134a-0fac-4c6f-86bc-18780e8e0a45" } }

Delete a label

Removes the label, not the file it points at. 404 if there is no such label. Needs your private key.

DELETE
1
https://api.fileboy.io/labels/:name

URL Params

name string

The label's name.

Example

REQUEST
1
curl -X DELETE 'https://api.fileboy.io/labels/hero' -H 'Authorization: token 0000000....'
RESPONSE
1
// 204 No Content

Get usage

Files and videos you have, and everything stored (files, resized copies, video renditions, posters). limits are the ones set on your app (from your plan, or an agreement); null means none is set on the app, not unlimited. Needs your private key.

GET
1
https://api.fileboy.io/usage

Example

REQUEST
1
curl 'https://api.fileboy.io/usage' -H 'Authorization: token 0000000....'
RESPONSE
1
2
3
4
5
6
7
8
{
  "data": {
    "files": 120,
    "videos": 4,
    "storageBytes": 734003200,
    "limits": { "files": 1000, "videos": null, "storageBytes": 5368709120 }
  }
}

Get storage breakdown

Stored bytes by kind, your biggest files and videos, and one total a day. storageBytes is the same as GET /usage, and the kinds add up to it. Daily totals are recorded just after midnight UTC and kept for 400 days; days before tracking started are missing, never filled in. Needs your private key.

GET
1
https://api.fileboy.io/usage/storage

Query Params

days number, optional

How many days of daily totals to return, 1 to 365. Defaults to 30.

top number, optional

How many of the biggest files and videos to list, 1 to 50. Defaults to 10.

Response

kinds object

count and bytes for each of images, files (every other upload), videoOriginals, videoRenditions, videoPosters, videoCaptions, imageTransforms (cached resized and converted copies) and otherGenerated. Every kind is present.

largestFiles File[]

Your biggest files, biggest first, as GET /files returns them. Video parts and generated copies are not listed.

largestVideos object[]

The videos that store the most: storageBytes (all parts together), originalBytes, renditionBytes, posterBytes and captionBytes.

history object[]

date (a UTC day), storageBytes and bytes by kind, oldest first.

Example

REQUEST
1
curl 'https://api.fileboy.io/usage/storage?days=30&top=5' -H 'Authorization: token 0000000....'
RESPONSE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "data": {
    "storageBytes": 734003200,
    "limits": { "storageBytes": 5368709120 },
    "kinds": {
      "images": { "count": 96, "bytes": 210763776 },
      "files": { "count": 24, "bytes": 31457280 },
      "videoOriginals": { "count": 4, "bytes": 314572800 },
      "videoRenditions": { "count": 16, "bytes": 157286400 },
      "videoPosters": { "count": 4, "bytes": 1048576 },
      "videoCaptions": { "count": 2, "bytes": 20480 },
      "imageTransforms": { "count": 310, "bytes": 18853888 },
      "otherGenerated": { "count": 0, "bytes": 0 }
    },
    "largestFiles": [{ "id": "2a860257-…", "name": "brochure.pdf", "fileSize": 20971520, … }],
    "largestVideos": [{
      "id": "9b1c4d1e-…", "name": "launch.mp4", "status": "ready", "storageBytes": 134217728,
      "originalBytes": 83886080, "renditionBytes": 50069504, "posterBytes": 262144, "captionBytes": 0
    }],
    "history": [{ "date": "2026-11-13", "storageBytes": 731906048, "kinds": { "images": 208666624, … } }]
  }
}

Get usage alert settings

The app's usage alert settings, the current month (UTC) and the alerts recorded in it, newest first. See the usage alerts docs for when alerts fire. Needs your private key.

GET
1
https://api.fileboy.io/usage/alerts

Example

REQUEST
1
curl 'https://api.fileboy.io/usage/alerts' -H 'Authorization: token 0000000....'
RESPONSE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "data": {
    "settings": {
      "enabled": true,
      "thresholds": [80, 100],
      "metrics": { "files": true, "videos": true, "storage": true },
      "emailMembers": true,
      "recipients": [],
      "webhook": true
    },
    "period": { "key": "2026-11", "start": "2026-11-01T00:00:00Z", "end": "2026-12-01T00:00:00Z" },
    "alerts": [
      { "id": "8d2b…", "metric": "files", "threshold": 80, "used": 812, "limit": 1000, "notified": true, "email": "sent", "createdAt": "2026-11-14T10:05:00Z" }
    ]
  }
}

Change usage alert settings

Only the fields you send change; metrics only changes the limits it names. Answers with the same body as the GET. Needs your private key.

PATCH
1
https://api.fileboy.io/usage/alerts

Body

enabled boolean, optional

Off stops every alert.

thresholds number[], optional

1 to 5 whole percentages from 1 to 100. Sorted, duplicates removed.

metrics object, optional

files, videos and storage, each true or false.

emailMembers boolean, optional

Email everyone on the app.

recipients string[], optional

Up to 10 more email addresses. Replaces the list.

webhook boolean, optional

Send usage.threshold_reached webhooks.

Example

REQUEST
1
curl -X PATCH 'https://api.fileboy.io/usage/alerts' -H 'Authorization: token 0000000....' -H 'Content-Type: application/json' -d '{ "thresholds": [50, 80, 90, 100] }'
RESPONSE
1
// 200, the same body as GET /usage/alerts

Get an image's colour palette

The dominant colour, main colours with their share of the image, named swatches and a readable text colour for each. Worked out at upload, or on the first request for older images. 422 unsupported_image for files that are not JPEG, PNG, WebP, BMP or TIFF. Needs your private key; the palette transform serves the same palette on image URLs.

GET
1
https://api.fileboy.io/files/:id/palette

URL Params

id string

The file's id, or PUBLIC_KEY+label.

Query Params

colors number, optional, default is 6

How many main colours to list, 0 to 16.

Example

REQUEST
1
curl 'https://api.fileboy.io/files/FILE_ID/palette?colors=2' -H 'Authorization: token 0000000....'
RESPONSE
1
2
3
4
5
6
7
8
9
10
11
12
{
  "data": {
    "version": 1,
    "dominant": { "hex": "#3d5a73", "rgb": [61, 90, 115], "share": 0.4213, "text": "#ffffff", "contrast": 7.22 },
    "colors": [
      { "hex": "#3d5a73", "rgb": [61, 90, 115], "share": 0.4213, "text": "#ffffff", "contrast": 7.22 },
      { "hex": "#e8d9c4", "rgb": [232, 217, 196], "share": 0.2876, "text": "#000000", "contrast": 15.15 }
    ],
    "swatches": { "vibrant": { "hex": "#c2452d", ... }, "darkVibrant": null, "lightVibrant": { "hex": "#e8d9c4", ... }, "muted": { "hex": "#3d5a73", ... }, "darkMuted": null, "lightMuted": null },
    "transparentShare": 0
  }
}

Errors

Errors are JSON,

{ "error": "file_not_found", "message": "…" }
. Codes: unauthorized (401), private_key_required, demo_app and folder_limit_reached (403), file_not_found, video_not_found, label_not_found, folder_not_found and link_not_found (404), label_taken, folder_exists, folder_not_empty, folder_too_large and too_many_links (409), invalid_id, invalid_json, invalid_parameter, invalid_cursor, invalid_label, invalid_tag, invalid_folder_name, invalid_folder_path, invalid_move, folder_too_deep, invalid_transform, validation_error, empty_update and too_many_ids (400), storage_error (500). The libraries raise a not found, permission or validation error you can catch by kind.

hello@fileboy.io

Fileboy

HomeAboutPricingChangelog
© Copyright 2026, CD2 Solutions Ltd.
Terms and ConditionsPrivacy Policy