Downloading

These routes take no API key. The 32-byte token in the URL is the credential, because recipients are people who have never heard of your instance.

Transfer contents

GET /api/download/:token
{
  "data": {
    "subject": "Proposal & draft contract",
    "message": "As discussed — feedback by Friday would be great.",
    "senderName": "Florian Strasser",
    "layout": "list",
    "locked": false,
    "requiresPassword": false,
    "expiresAt": "2026-09-01T10:00:00.000Z",
    "totalSize": 574464,
    "files": [
      { "id": 41, "filename": "Proposal-2026-118.pdf", "size": 421888, "isImage": false }
    ]
  }
}

When the transfer is password protected and not yet unlocked, locked is true and the file list is omitted entirely — no names, no sizes, no message. A list of filenames is often as revealing as the files themselves.

isImage is decided by the file's own leading bytes, not by its name or the type the uploader declared.

Unlocking

POST /api/download/:token/unlock
{ "password": "grape" }

Sets a cookie scoped to that one transfer. It cannot be carried to another — unlocking one protected transfer does not unlock the rest.

Every attempt counts against the download rate limiter, which is what caps online guessing of a deliberately short share password.

One file

GET /api/download/:token/file/:fileId

Served as application/octet-stream with the original filename in Content-Disposition, so an uploaded .html cannot execute on your origin.

Everything as a zip

GET /api/download/:token/zip

Streamed and stored rather than compressed. The download starts immediately instead of making the recipient wait while the server packs an archive — and most of what people send is already compressed, so deflating it again would cost time to save nothing.

Image previews

GET /api/download/:token/preview/:fileId

The gallery's thumbnails, and the one place an uploaded file is rendered in a browser rather than saved. It is the strictest endpoint in the app:

  • the leading bytes must match a known raster image signature;
  • the Content-Type sent is the one derived from those bytes;
  • nosniff stops the browser second-guessing it;
  • a sandbox CSP strips scripting and same-origin privileges regardless.

Anything that is not a real image answers 404. SVG is deliberately excluded: it is an image, but it is also a document that can carry scripts.

Expiry

Every one of these routes refuses an expired transfer the moment it lapses, independently of the scheduled sweep. An expired, deleted or never-existent token all answer 404, so an old link reveals nothing about what used to be there.