FileTransfer

API reference

FileTransfer API

Complete reference of the endpoints the server exposes.

Base. All addresses are relative to the service origin, for example https://your-domain/upload.

Formats. Responses are JSON, except for endpoints that serve a file or an HTML page. JSON request bodies expect the Content-Type: application/json header.

One API address. All administration goes through POST /admin/api, with an action field naming the operation. File transfers have their own addresses, because a browser must be able to follow them directly.

Streaming. Uploads and downloads are streamed, with no size limit on the application side. Any ceiling comes from the reverse proxy, see Limits.

Encryption. Storage is encrypted, with no consequence for the API: reported sizes are those of the original file, and Range requests are still served, so resumable downloads and seeking inside a video keep working.

Authentication

Public endpoints require nothing. Everything administrative requires a session.

A successful sign-in sets an admin_session cookie, HttpOnly and SameSite=Lax, marked Secure behind HTTPS. It is out of reach of the page JavaScript: only the hash of the token is kept in the database.

Send that cookie on every call. With fetch that means credentials: 'same-origin'; with curl, a cookie jar.

Sessions last 7 days and are extended on each use. Without a valid cookie, API calls answer 401 and pages redirect to /admin/login.

# Open a session
curl -c cookies.txt -X POST https://your-domain/admin/api \
     -H 'Content-Type: application/json' \
     -d '{"action":"login","username":"admin","password":"…"}'

# Reuse the session
curl -b cookies.txt -X POST https://your-domain/admin/api \
     -H 'Content-Type: application/json' \
     -d '{"action":"overview"}'

If the account requires a passkey as a second factor, login sets no cookie and answers {"status":"PASSKEY_REQUIRED","pending":"…"}. You then run the ceremony described in Passkeys.

Public endpoints

Reachable without authentication.

POST /upload

Uploads a file. Body in multipart/form-data, field file. Content is written to disk as it is read: size is not limited by the application.

URL parameterPurpose
passwordOptional. Protects the file: downloading it will require this password.
curl -F "file=@report.pdf" https://your-domain/upload

{"id":"aB3xY9","size":184320,"ip":"203.0.113.7"}

Names starting with . or _ are ignored. An interrupted transfer leaves no partial file behind.

GET /{id}

With no parameter, serves the download page. An unknown identifier returns the 404 page.

ParameterEffect
jsonInfo=onlyReturns {"filename":…,"size":…} without serving the file.
download=trueServes the file as an attachment.
preview=trueServes the file for direct display in the browser.
passwordThe file password, if it is protected.

The password can also travel in the X-File-Password header, which is what the download page does. If it is missing or wrong, the response is 401.

curl "https://your-domain/aB3xY9?jsonInfo=only"
curl -H "X-File-Password: secret" -O -J "https://your-domain/aB3xY9?download=true"

Range requests are accepted: resumable downloads and seeking inside a video both work.

Link previews — Open Graph

The page served by /{id} carries the file metadata, so a link pasted into Discord, Slack or iMessage renders as a card rather than a bare URL. These clients read the <meta> tags without running the page JavaScript.

File typeTags emitted
Imageog:image, og:image:type, twitter:card=summary_large_image — the thumbnail is shown
Videoog:video, og:video:type, twitter:card=player
Audioog:audio, og:audio:type
OtherSummary card: og:title, og:description

A password-protected file exposes no preview URL: only its name, its size and a note that it is protected. The preview would otherwise be reachable without the password.

<meta property="og:title" content="report.pdf">
<meta property="og:description" content="1.67 Mo">
<meta property="og:image" content="https://your-domain/aB3xY9?preview=true">

GET /  ·  /index  ·  /api

The upload page, its alias, and this page.

Administration pages

AddressDescription
/adminRedirects to the dashboard.
/admin/loginSign-in. Redirects to the dashboard if a session is already open.
/admin/registerAccount creation from an invitation code.
/admin/dashboardDashboard. Redirects to sign-in without a session.

/admin/api actions

All POST, JSON body containing at least action. Session required, except for login and register.

Session and account

ActionFieldsResponse
loginusername ou email, passwordSets the session cookie. PASSKEY_REQUIRED + pending if a second factor is required.
registerusername, email, password, firstName, lastName, inviteCodeCreates the account and opens the session. Password of 8 characters minimum.
logout—Revokes the current session and clears the cookie.
me—The signed-in account, passkey count, personal cloud usage.
change_passwordcurrentPassword, newPasswordChanges the password and signs out the other devices.
set_auth_policyauthPolicypassword, passkey_or_password or password_and_passkey. The last two require a registered passkey.
sessions_list—Active sessions of the account, the current one marked current.
session_revokesessionIDSigns out one specific session.
sessions_revoke_others—Signs out all other devices.
passkeys_list—The account passkeys.
passkey_renamecredID, credNameRenames a passkey.
passkey_deletecredIDRevokes a passkey. Removing the last one drops the account back to password-only.

Public files

ActionFieldsResponse
list_filessearch; offset and limit as URL parameters{files, total}. Search covers the name, the identifier and the uploader IP, and accepts the * and ? wildcards.
renamefileID, newFileNameRenames the file on disk and in the database.
change_idfileID, newFileIDChanges the public identifier, hence the share link.
deletefileIDDeletes the file and its record.
overview—Instance counters.
storage—Storage usage and ceiling.

Personal cloud

Partitioned per account: queries filter on the owner, so one administrator cannot reach another one's space.

ActionFieldsResponse
personal_listfolder{folder, folders, files} of the requested folder.
personal_createfolder, fileNameCreates an empty file.
personal_renamefileID, newFileNameRenames a file.
personal_movefileID, folderMoves a file to another folder.
personal_deletefileIDDeletes a file.
personal_sharefileID, sharePasswordPublishes the file into the public space. Returns {id, url, protected}. Empty password for an open link.
folder_createfolderCreates a folder, even an empty one.
folder_renamefolder, newFileNameRenames a folder; subfolders and files follow.
folder_deletefolderDeletes a folder and all its contents. Returns the number of files removed.

Administrators, invitations, journal

ActionFieldsResponse
admins_list—Accounts, sign-in method, passkeys, sessions.
admin_deleteadminIDDeletes an account with its sessions and passkeys. Neither your own account nor the last one.
invites_list—Invitation codes and their state.
invite_create—Generates a single-use code.
invite_deletetokenDeletes an unused code.
audit_listcategory, level, search, offset, limit{entries, total}. Categories: auth, upload, download, file, personal, admin, system. Levels: info, warn, error.
bans_list—Bans and the settings in force.
ban_createipAddr, minutes, reasonBans an address. Refused if it is on the allowlist.
ban_revokeipAddrLifts a ban and resets that address's attempt counter.
ban_settings_savethreshold, window, minutes, allowlistTunes the automatic ban.
admin_createtarget: first name, last name, username, email, passwordCreates an account without an invitation.
admin_updateadminID, targetEdits an account's name and credentials.
admin_set_passwordadminID, target.passwordForces a password and closes that account's sessions.
audit_stats—Event count per category over 24 hours.

File transfers

Addresses separate from /admin/api, so a browser can follow them directly. Session required.

GET /admin/download?id=…

Downloads a public file from the administration. The file password, if any, is not asked for: this is an assumed administrative privilege, and every such access is recorded in the journal with the account name.

POST /admin/personal/upload?folder=…

Uploads a file into the personal cloud. multipart/form-data, field file. No size limit on the application side.

GET /admin/personal/download?id=…

Serves a personal file. preview=true returns it for direct display with its real MIME type, which is what media preview and the editor use.

GET /admin/personal/zip?folder=…

Returns a folder and its subfolders as a zip archive, built as a stream, with no temporary file and no memory buffer. The tree relative to the requested folder is preserved.

POST /admin/personal/save?id=…

Replaces the content of a personal file. Raw body, 2 MiB maximum. The write goes through a temporary file followed by a rename: an interrupted or refused save leaves the original intact.

Passkeys (WebAuthn)

Each ceremony runs in two calls. The ceremony token returned by begin is single-use and expires after five minutes.

AddressDescription
POST /admin/webauthn/register/beginSession required. Returns {ceremony, options}. A resident key is required, which is what makes password managers store it as a passkey.
POST /admin/webauthn/register/finish?ceremony=…&name=…Session required. Body: the attestation response from the browser. Registering the first passkey moves the account to passkey_or_password.
POST /admin/webauthn/login/beginNo session. Discoverable ceremony, no username. With ?pending=…, second factor after an accepted password.
POST /admin/webauthn/login/finish?ceremony=…Body: the assertion response. Sets the session cookie. Add &pending=… to complete a two-factor sign-in.

Errors

An error returns an explicit HTTP status and an {"error":"…"} body.

StatusMeaning
400Malformed request, missing field or rejected value.
401Missing or expired session; or missing or wrong file password.
403The account sign-in method does not allow this authentication means.
404File or resource not found.
409Username or email address already taken.
413Content too large. Often returned by the reverse proxy, not the application.
500Server error. The journal keeps the cause.

Limits

Transfer size. The application imposes no ceiling: the request body is written to disk as it is read. In practice the reverse proxy is what limits you. Under Nginx, client_max_body_size 0 lifts the limit and proxy_request_buffering off avoids spooling the whole file onto the proxy disk before forwarding.

Text editor. 2 MiB, on the server as in the browser.

Sessions. 7 days, extended on each use.

Journal. Kept 90 days by default, tunable with AUDIT_RETENTION_DAYS.

Encryption. Files are encrypted on disk with AES-256-GCM, and compressed with zstd when the content benefits from it. All of it is transparent to the API: reported sizes and served bytes are those of the original file. Encryption protects against a stolen disk or a leaked backup, not against someone who controls the server, since the key lives there. A file password protects its share link, not its content against whoever runs the machine.