homeaboutvideotry it nowpricingdocs
Docs

Getting Started

Signed URLs

Serve private files, images and videos only to the people you choose. Your server signs a URL that stops working after a time you set, and Fileboy refuses everything else.

How it works

Every app is public by default: anyone with the URL of a file, image or video can read it. There are two ways to need a signature:

  • The whole app: switch on Read access: signed URLs only in the app's settings (or with the API below). Every file, image and video in the app needs a signed URL, including the ones already uploaded.
  • Single files: set a file's read permission to Signed URLs only on its page in the dashboard. Only that file needs a signed URL; the rest of the app stays public. The app's default read permission decides what new uploads start with, so with it set to Signed URLs only, new files need signed URLs while existing files keep their own setting. Videos have no permission of their own: they only need a signature when the whole app does.

A read that needs a signature is allowed with one of:

  • a signed URL:
    fb_exp
    (when it expires) and
    fb_sig
    (a signature made with your private key) in the query string, or
  • your app's private key, as an
    Authorization: token ...
    header, for your own server fetching a file.

Signing happens on your server and needs no request to Fileboy. The private key never goes to the browser, only the finished URL does.

Turning the setting on stops every existing unsigned link working at once, in pages, emails and caches you cannot reach. Switch your site over to signed URLs first, then turn it on. The same goes for a single file: setting it to Signed URLs only stops its existing unsigned links working.

Signatures are made the same way in both cases, with the app's private key. A signature on a public file of a public app is simply ignored, so you can sign every URL you hand out without checking which files need it.

Sign a URL

Give the SDK the URL of any file, image or transform and an expiry (an hour if you do not say). You get the same URL back with the signature added. It works for

/files/get
,
/images/get
(every transform), file data and video URLs, and for label URLs (
PUBLIC_KEY+label
). It also signs a file's share page link,
https://fileboy.io/f/FILE_ID
, so a private file can be sent with a preview and a download button.

// on your server, with the private key
const fileboy = Fileboy({ publicKey, privateKey })

// any file URL, image URL or transform URL. Expires in 10 minutes.
const url = fileboy.signUrl(
  fileboy.buildImageUrl(id, { transforms: [{ type: "resize", width: 400, height: 400 }] }),
  { expiresIn: 600 },
)
// https://api.fileboy.io/images/get/2a86...?transform=resize:400x400&fb_exp=1800000000&fb_sig=...

// only this address can use it
const locked = fileboy.signUrl(fileboy.buildFileUrl(id), { expiresIn: 60, ip: "203.0.113.7" })

// buildFileUrl and buildImageUrl add a short-lived token to the URL when the client has a
// private key. signUrl removes it: a signed URL does not need it.

A signature is bound to what you signed: the file, the exact

transform
, the file name in the path, the expiry and the address, if any. Changing any of them, for example asking for a bigger size, is refused with 403. Sign each size you use. The
download
flag is not signed, it only changes how the file is presented.

Video

A video is many requests: the manifest, a playlist per quality, the segments, the poster and the captions, and a player builds most of those URLs itself. So one signature covers everything of one video. Fileboy passes it on inside the HLS and DASH manifests, so the player keeps working with the single query string you give it.

// on your server. One signature covers the manifest, segments, poster and captions.
// Make it last longer than the video takes to watch.
const signedQuery = fileboy.signVideo(videoId, { expiresIn: 4 * 3600 })

// in the page: <fileboy-player video-id="${videoId}" signed-query="${signedQuery}"></fileboy-player>
// or any video URL: `https://api.fileboy.io/videos/${videoId}/manifest?${signedQuery}`

With

<fileboy-player>
, set it as the
signed-query
attribute. Make it last longer than the video takes to watch: a request after the expiry is refused, and a player cannot ask you for a new one part way through.

For a private app the manifests never point at the public CDN: every segment is served through Fileboy, where the signature is checked, and counts towards your transfer like any other read.

Turn it on

In the dashboard it is Settings, Read access. From code it needs the private key:

// needs the private key. Every unsigned link stops working at once.
await fileboy.setRequireSignedReads(true)
await fileboy.getRequireSignedReads() // true

See the API reference for

GET /app/settings
and
PATCH /app/settings
.

Limit a URL to one address

Pass

ip
with an address or a CIDR range such as
203.0.113.0/24
and the URL only works from there. It is part of the signature, so it cannot be removed or widened. Fileboy reads the address from the connection it sees, through its own gateway, so this suits server to server use better than visitors behind mobile networks or VPNs, whose address can change between requests.

What you get back

In an app that requires signed URLs the answer is the same whether or not the file exists, so unsigned requests cannot be used to find out what you store. (In a public app, where a public file or label already answers anyone, a label that does not exist is a 404 and one on a Signed URLs only file is a 401.) Refusals are never cached.

  • 401 signature_required
    : no signature, or no expiry.
  • 403 signature_invalid
    : the signature is not valid for this file, transform, file name or address, or was made with another key.
  • 403 signature_expired
    : the expiry has passed (30 seconds of leeway are allowed for clocks that disagree). Only reported for a signature that is otherwise valid.
  • 403 ip_not_allowed
    : the URL is limited to another address.
  • 404
    : a signed request for something that does not exist, as ever.

Caching

Private responses are sent with

Cache-Control: private, max-age=N
where N is the time left before the signature expires, so a browser stops reusing them when the URL stops working and no shared cache or CDN stores them. Do not put a cache in front that ignores the query string, a cache that ignores
fb_sig
would serve a private file to everyone. Transformed copies Fileboy keeps itself are checked on every read like the original.

Keys and rotation

Signatures are made with the app's private key. Regenerating the private key on the Keys page stops every signed URL made with the old key straight away, which is what you want after a leak. To roll the key without breaking pages that are already open, choose Rotate instead: signed URLs made with the old key keep working for 7 days while you deploy the new key. The old key can only verify signatures, it cannot manage files. Regenerating the public key changes label URLs, which include it.

Sign without an SDK

The signature is the base64url (no padding) HMAC-SHA256, keyed with the private key, of this text, with a newline between lines and none at the end:

1
2
3
4
5
6
7
FBS1
scope=file
id=2a860257-ecf9-424a-846e-00ab68e2fd58
transform=resize%3A100x100
filename=
exp=1800000000
ip=
  • scope
    is
    file
    for
    /files/get
    and
    /images/get
    ,
    file-data
    for
    /files/data
    and
    video
    for everything under
    /videos/id
    .
  • id
    is the file or video id in lower case, or
    PUBLIC_KEY+label
    as it is in the URL.
  • transform
    and
    filename
    are the decoded values, empty when the URL has none. The first
    transform
    parameter counts.
  • exp
    is the expiry in unix seconds and
    ip
    the address or range, or empty.
  • Every value is percent-encoded byte by byte (UTF-8), keeping only
    A-Z a-z 0-9 - . _ ~
    , with upper case hex.

Add

fb_exp
,
fb_ip
(if used) and
fb_sig
to the URL's query. The libraries all test against one shared set of cases so they produce identical signatures, and the cURL sample above signs with only
openssl
.

Good to know

  • Single files can be made private with their own read permission. Single videos cannot yet: a video needs a signature only when its whole app does.
  • Making a file private stops new unsigned reads at once. Public files are sent with a private cache header, so CDNs and shared caches do not keep them, but a browser that downloaded the file while it was public can keep its own copy.
  • A signed URL is a bearer link: anyone who has it can use it until it expires. Keep expiries short, and add an address limit where you can.
  • The old
    authorization
    token in a query string is not accepted for reads that need a signature, only the header, because it does not expire the way a signed URL does.
  • Video made before this feature may also exist at its storage address on the CDN. Manifests for private apps never reveal it, but an object whose address was already shared stays reachable there.

Signed URLs

How it worksSign a URLVideoTurn it onLimit a URL to one addressWhat you get backCachingKeys and rotationSign without an SDKGood to know
hello@fileboy.io

Fileboy

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