v1 · REST · JSON

API reference

Uploads, file management, share links and webhooks over one REST API. Every endpoint lives under https://filemv.com/api, with JSON bodies.

Quickstart — your first upload in three steps

An upload is two separate calls. POST /api/uploads reserves a session and returns an uploadUrl. Send the file's bytes to that URL with a PUT request. That request needs no API key: the session id authorizes it. Finally, call GET /api/uploads/{sessionId}/status until the status is COMPLETE.

The bytes always go to https://filemv.com/files/…. For files over 100 MiB, the resumable /files/tus protocol is recommended.

import { readFile } from "node:fs/promises";

const base = "https://filemv.com";
const auth = { Authorization: `Bearer ${process.env.FILEMV_API_KEY}` };
const bytes = await readFile("video.mp4");

// 1. Reserve an upload session
const res = await fetch(`${base}/api/uploads`, {
  method: "POST",
  headers: { ...auth, "Content-Type": "application/json" },
  body: JSON.stringify({ fileName: "video.mp4", contentType: "video/mp4", fileSizeBytes: bytes.length }),
});
const { sessionId, uploadUrl } = await res.json();

// 2. Send the bytes to uploadUrl (no API key needed: the session id authorizes it)
await fetch(uploadUrl, { method: "PUT", body: bytes, headers: { "Content-Length": String(bytes.length) } });

// 3. Poll until COMPLETE
const status = await fetch(`${base}/api/uploads/${sessionId}/status`, { headers: auth }).then((r) => r.json());
console.log(status.status, status.fileId);

Authentication

External applications should use an API key: send it on every request in the Authorization: Bearer fmv_… header. The FileMv web app signs in with a session cookie. Every endpoint accepts both.

API keyrecommended

Authorization: Bearer fmv_…

600 requests/minute per key. Available on every plan, at most 10 active keys per user. Revocable at any time.

Browser session (cookie)

auth_token · X-XSRF-TOKEN

The web app signs in with an HttpOnly cookie. Requests that change data need an X-XSRF-TOKEN header. Don't use this for your own integration.

Create a key in the app

  1. Open Settings → API keys (/settings/api-keys).
  2. Optionally enter a label (up to 80 characters), then click Create key.
  3. The full key is shown only once, so copy it right away. After that the list shows only its start (for example fmv_AbC12x…), the label, when it was created and when it was last used.
  4. Revoke a key from its row. You can have at most 10 active keys; beyond that, creating one fails with 409 api_key_limit_reached.

Create a key via the API

The body of POST /api/me/api-keys is optional, for example {"label": "CI server"}. The response is 201 and contains the rawKey field. You can never retrieve it again, because the server stores only a keyed hash of the key. GET lists your keys, and DELETE /api/me/api-keys/{id} revokes one with 204.

Format and usage

A key is the fmv_ prefix followed by 40 URL-safe base64 characters (~240 bits). The server treats only tokens starting with Bearer fmv_ as API keys. With an unknown or revoked key, protected endpoints return 401 authentication_required, and public endpoints run as if you had sent no key. Send one credential per request: either a key or a cookie.

A key carries full authority

There are no scopes: a key can do everything its owner can. It reaches files, shares, webhooks, key management and account actions — and, if the owner is an admin, the admin endpoints too. Treat it like a password.

Good practice

  • Keep it in an environment variable or a secret store. Never put it in client-side or mobile code, or in a repository.
  • To rotate: create a new key, switch the integration to it, then revoke the old one.
  • Watch lastUsedAt to spot unexpected use. Give every integration its own key.

Rate limits

TierLimitCounted per
apikey600 / minAPI key
api60 / minIP address (requests without a key)
auth10 / minIP address, /api/auth/**

Every /api response carries X-RateLimit-Limit (budget per minute), X-RateLimit-Remaining (requests left) and X-RateLimit-Reset (seconds until the budget is full again). Over the limit you get 429 with a Retry-After header and the rate_limited code.

Some features have their own limits on top, answered with 429 too_many_requests:

  • Anonymous upload: 5 per hour per IP (up to 10 files per batch, up to 100 MiB per file).
  • Unlocking a password-protected share: 5 attempts per 15 minutes per share and IP.
  • Resolving a share: 40 per minute per IP; refreshing download URLs: 30 per minute.
  • Creating, testing and rotating webhooks: 10 per minute per user.

Error codes

Every error has the same shape: { "timestamp", "status", "error", "code", "message" }. Validation errors add an errors list. Branch on the code field in your code.

HTTPcodeWhen
400validation_failed, bad_requestA field in the request is missing or invalid
401authentication_requiredMissing or invalid API key or session
403access_deniedAnother user's resource
404*_not_foundUnknown id or token
409 storage_quota_exceeded, api_key_limit_reached, vanity_slug_taken, webhook_limit_reached Storage quota, key or webhook limit, slug already taken
413payload_too_largeThe file is larger than allowed
429rate_limited, too_many_requestsRate limited — wait as Retry-After says

Uploads

POST/api/uploadsAPI key or session

Create an upload session for an authenticated user

Reserves an upload session for the authenticated user and returns the uploadUrl to PUT the file's bytes to. When parentFolderId is supplied, the folder is validated to be owned by the caller before any session is reserved. When autoShare is set, a link-only (SHARED) share token and its short link are minted up front and returned with the reservation. Adding publicListing=true to autoShare makes that share PUBLIC (listed: the share page may be indexed by search engines) instead of SHARED.

Request bodyapplication/json

  • autoShareboolean Optional. When true, a link-only (SHARED) share is minted for this upload, and the response carries its token and short link before the bytes move. Defaults to false: the file stays PRIVATE.
  • contentTypestringrequired MIME content type as detected by the client. Required.
  • fileNamestringrequired Display name of the file being uploaded. Required.
  • fileSizeBytesinteger Declared file size in bytes. Must be > 0.
  • parentFolderIdstring Optional Drive folder UUID to upload into. When omitted the file lands at the user's Drive root. Validated server-side against the authenticated user's tree before any session is reserved.
  • publicListingboolean Optional, only honoured together with autoShare=true. When true the minted share is PUBLIC (listed) instead of link-only SHARED: the share page may be indexed by search engines and the file is returned by FileMv's own cross-owner search. Ignored when autoShare is not set (the upload stays PRIVATE). Defaults to false.

Responses

  • 201Session created — body carries sessionId and uploadUrl, plus the share fields when autoShare was set
  • 400Validation failed (blank fileName/contentType, non-positive size)
  • 401Missing or invalid Bearer token
  • 404parentFolderId is unknown, owned by another user, or refers to a FILE
  • 409Upload would exceed the user's storage quota
  • 413Declared fileSizeBytes exceeds the per-file limit of the caller's plan (freeUserMaxBytes / paidUserMaxBytes in GET /api/public/config) — code=payload_too_large

POST/api/uploads

const res = await fetch("https://filemv.com/api/uploads", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "contentType": "image/jpeg",
    "fileName": "vacation.jpg",
    "fileSizeBytes": 1048576
  }),
});
const data = await res.json();
Response201application/json
{
  "directUrl": null,
  "fileId": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
  "sessionId": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
  "shareToken": null,
  "shortUrl": null,
  "uploadUrl": "https://filemv.com/files/2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

GET/api/uploads/{sessionId}/statusAPI key or session

Look up the current status of an upload session

Returns the session id, its state (PENDING / UPLOADING / COMPLETE / FAILED), and — once complete — the resulting fileId + fileName. Ownership is enforced: a session may only be read by the user who created it.

Parameters

NameInTypeDescription
sessionId requiredpathstringUpload session UUID returned by POST /api/uploads

Responses

  • 200Current session status
  • 400Malformed sessionId
  • 401Missing or invalid Bearer token
  • 403Session is owned by a different user
  • 404Unknown sessionId
  • 502Storage worker rejected this service's own credential, or answered an unresolvable status

GET/api/uploads/{sessionId}/status

const res = await fetch("https://filemv.com/api/uploads/3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77/status", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
const data = await res.json();
Response200application/json
{
  "fileId": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
  "fileName": "vacation.jpg",
  "sessionId": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
  "status": "COMPLETE"
}
Try it

This call needs parameters — run it from your own environment with the sample above.

POST/api/public/uploadspublic5/hour/IP · captcha may be required

Create an anonymous upload session

Reserves an upload session for an unauthenticated uploader, mints a share token, and applies the configured retention. A missing/rejected captcha surfaces as 400 before any session is reserved. publicListing=true creates the share PUBLIC (listed, indexable by search engines); absent or false keeps the link-only SHARED default.

Request bodyapplication/json

  • captchaTokenstring Cloudflare Turnstile token. Required when filemv.captcha.enabled=true; tolerated blank in dev/test.
  • contentTypestringrequired MIME content type as detected by the client. Required.
  • fileNamestringrequired Display name of the file being uploaded. Required.
  • publicListingboolean Optional. When true the share is created PUBLIC (listed): the share page may be indexed by search engines and the file is returned by FileMv's own search. Absent or false keeps the link-only SHARED default (unlisted, noindex).
  • sizeBytesinteger Declared file size in bytes. Must be > 0.

Responses

  • 201Session created — body carries the upload URL and the share token
  • 400Validation failed (code validation_failed: blank fileName/contentType, non-positive size) or captcha missing/rejected (code captcha_failed, message "Captcha verification failed")
  • 429Per-IP anonymous-upload rate-limit exceeded

POST/api/public/uploads

const res = await fetch("https://filemv.com/api/public/uploads", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "contentType": "image/png",
    "fileName": "screenshot.png",
    "sizeBytes": 524288
  }),
});
const data = await res.json();
Response201application/json
{
  "directUrl": null,
  "fileId": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
  "sessionId": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "shortUrl": "https://filemv.com/…",
  "uploadUrl": "https://filemv.com/…"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

POST/api/public/uploads/batchpublic5/hour/IP · captcha may be required

Create anonymous upload sessions for a batch of files

Multi-file twin of POST /api/public/uploads. Verifies the captcha token ONCE, consumes ONE per-IP rate-limit token for the whole batch, validates every file's size cap before reserving anything, then reserves one upload session per file (own share token and short link each) and returns them in request order. Batch size is capped by anonymousMaxFilesPerBatch (/api/public/config). publicListing=true creates every share PUBLIC (listed, indexable by search engines); absent or false keeps the link-only SHARED default.

Request bodyapplication/json

  • captchaTokenstring Cloudflare Turnstile token, verified ONCE for the whole batch. Required when filemv.captcha.enabled=true; tolerated blank in dev/test.
  • object[]required Files to reserve sessions for, in display order. At least one; at most anonymousMaxFilesPerBatch.
  • publicListingboolean Optional. When true every file's share is created PUBLIC (listed): the share page may be indexed by search engines and the file is returned by FileMv's own search. Absent or false keeps the link-only SHARED default (unlisted, noindex).

Responses

  • 201Sessions created — body carries one single-file response per file, in request order
  • 400Validation failed (empty list, blank fileName/contentType, non-positive size, more files than anonymousMaxFilesPerBatch) or captcha missing/rejected (code captcha_failed)
  • 413At least one file exceeds the anonymous per-file size cap — nothing was reserved
  • 429Per-IP anonymous-upload rate-limit exceeded

POST/api/public/uploads/batch

const res = await fetch("https://filemv.com/api/public/uploads/batch", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "files": [
      {
        "contentType": "image/jpeg",
        "fileName": "nyaralas-2026.jpg",
        "sizeBytes": 524288
      }
    ]
  }),
});
const data = await res.json();
Response201application/json
{
  "files": [
    {
      "directUrl": null,
      "fileId": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
      "sessionId": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
      "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
      "shortUrl": "https://filemv.com/…",
      "uploadUrl": "https://filemv.com/…"
    }
  ]
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

Files

GET/api/filesAPI key or session

List direct children of a folder (or root items), or search files across all folders

Returns the direct children of the requested folder, or the user's root items when parentId is omitted. Folder rows carry a recursive size rollup; file rows carry their own size + metadata. The optional ?sort= parameter selects the result ordering — see the parameter doc for the accepted vocabulary. Unknown values raise 400. The optional ?q= parameter switches to a folder-independent file search: when non-blank, parentId is ignored and the response contains only the caller's own non-deleted files (across every folder) whose name contains q case-insensitively.

Parameters

NameInTypeDescription
parentId querystringUUID of the parent folder. Omit (or null) to list root items. Ignored when `q` is provided.
limit queryintegerPage size. Server-side maximum applies.
page queryinteger1-based page number.
sort querystringResult ordering. Accepted values: `type,name` (default — folder-first-then-alpha, preserves historical behaviour); `name` / `-name` (ascending / descending by name, mixed folders + files); `createdAt` / `-createdAt` (by creation timestamp, mixed); `size` / `-size` (FILES only — folders are surfaced first by name, files after by size). Unknown values raise 400.
q querystringOptional folder-independent file-name search term. When non-blank, `parentId` is ignored and the response lists only the caller's own non-deleted files (across all folders) whose name contains this value case-insensitively. Blank / absent keeps the parentId-scoped folder listing.

Responses

  • 200Paginated list of file-system items
  • 400Unsupported `sort` value
  • 401Missing or invalid Bearer token
  • 404parentId is unknown or owned by a different user

GET/api/files

const res = await fetch("https://filemv.com/api/files", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
const data = await res.json();
Response200application/json
{
  "files": [
    {
      "commentsCount": 1,
      "contentKind": "FILE",
      "deletedAt": "2026-10-04T09:30:00Z",
      "downloadCount": 1,
      "id": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
      "likesCount": 1,
      "mimeType": "string",
      "name": "vacation.jpg",
      "parentId": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
      "previewUrl": "https://filemv.com/…",
      "shareMode": "string",
      "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
      "size": 1048576,
      "type": "file",
      "uploadedAt": "2026-10-04T09:30:00Z"
    }
  ],
  "pagination": {
    "currentPage": 1,
    "totalItems": 1,
    "totalPages": 1
  }
}
POST/api/files/foldersAPI key or session

Create a new folder under the caller's tree

Creates a folder at the root (parentId omitted) or under an existing folder owned by the caller.

Request bodyapplication/json

  • namestringrequired Display name for the new folder. Required. Max 255 chars; must not contain path separators.
  • parentIdstring Optional parent folder UUID. Omit to create the folder at the user's Drive root.

Responses

  • 201Folder created
  • 400Validation failed (blank name)
  • 401Missing or invalid Bearer token
  • 404parentId is unknown, owned by a different user, or refers to a FILE
  • 409A sibling with the same name already exists in the destination

POST/api/files/folders

const res = await fetch("https://filemv.com/api/files/folders", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "name": "Holiday 2025",
    "parentId": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091"
  }),
});
const data = await res.json();
Response201application/json
{
  "commentsCount": 1,
  "contentKind": "FILE",
  "deletedAt": "2026-10-04T09:30:00Z",
  "downloadCount": 1,
  "id": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
  "likesCount": 1,
  "mimeType": "string",
  "name": "vacation.jpg",
  "parentId": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
  "previewUrl": "https://filemv.com/…",
  "shareMode": "string",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "size": 1048576,
  "type": "file",
  "uploadedAt": "2026-10-04T09:30:00Z"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

PATCH/api/files/moveAPI key or session

Move several items to the same destination in one request

Always returns 200 on a well-formed request; per-item outcomes are partitioned into 'moved' and 'failed'. Malformed input (empty ids, no destination) still returns 400.

Request bodyapplication/json

  • idsstring[]required UUIDs of the file-system items to move. Must contain at least one entry.
  • moveToRootboolean Move every item to the user's Drive root. Mutually exclusive with parentId.
  • parentIdstring Destination parent folder UUID. Mutually exclusive with moveToRoot.

Responses

  • 200Per-item success / failure breakdown
  • 400Validation failed — empty ids, or neither parentId nor moveToRoot supplied
  • 401Missing or invalid Bearer token

PATCH/api/files/move

const res = await fetch("https://filemv.com/api/files/move", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "ids": [
      "6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b",
      "9a8b7c6d-5e4f-4321-9876-0fedcba98765"
    ],
    "parentId": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091"
  }),
});
const data = await res.json();
Response200application/json
{
  "failed": [
    {
      "id": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
      "message": "string",
      "status": 1
    }
  ],
  "moved": [
    {
      "commentsCount": 1,
      "contentKind": "FILE",
      "deletedAt": "2026-10-04T09:30:00Z",
      "downloadCount": 1,
      "id": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
      "likesCount": 1,
      "mimeType": "string",
      "name": "vacation.jpg",
      "parentId": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
      "previewUrl": "https://filemv.com/…",
      "shareMode": "string",
      "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
      "size": 1048576,
      "type": "file",
      "uploadedAt": "2026-10-04T09:30:00Z"
    }
  ]
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

GET/api/files/trashAPI key or session

List soft-deleted files owned by the caller

Newest-trashed-first. When a folder is trashed each descendant file lands here as its own row. Folder rows are listed only with includeFolders=true , and then only the folders trashed directly (a sub-folder trashed together with its parent comes back with the parent's restore).

Parameters

NameInTypeDescription
includeFolders querybooleanAlso list the folders the caller trashed directly (type `folder`, `size` null). Default `false`: files only.

Responses

  • 200Trashed rows, newest-trashed-first
  • 401Missing or invalid Bearer token

GET/api/files/trash

const res = await fetch("https://filemv.com/api/files/trash", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
const data = await res.json();
Response200application/json
[
  {
    "commentsCount": 1,
    "contentKind": "FILE",
    "deletedAt": "2026-10-04T09:30:00Z",
    "downloadCount": 1,
    "id": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
    "likesCount": 1,
    "mimeType": "string",
    "name": "vacation.jpg",
    "parentId": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
    "previewUrl": "https://filemv.com/…",
    "shareMode": "string",
    "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
    "size": 1048576,
    "type": "file",
    "uploadedAt": "2026-10-04T09:30:00Z"
  }
]
PATCH/api/files/{id}API key or session

Move and/or rename a file-system item

Body must supply at least one mutation — a move target (parentId or moveToRoot=true) or a non-null name. Move and rename can be combined in a single PATCH and are applied atomically.

Parameters

NameInTypeDescription
id requiredpathstringUUID of the item to update

Request bodyapplication/json

  • moveToRootboolean Move the item to the user's Drive root. Mutually exclusive with parentId.
  • namestring New display name for the item. Max 255 chars; must not contain path separators. Server-side sanitisation additionally trims whitespace and strips ASCII control characters.
  • parentIdstring Destination parent folder UUID. Mutually exclusive with moveToRoot.

Responses

  • 200The updated item
  • 400Validation failed (no mutation supplied, name too long)
  • 401Missing or invalid Bearer token
  • 404Item or destination parent is unknown / owned by another user
  • 409Name collision in the destination, or move would create a folder cycle

PATCH/api/files/{id}

const res = await fetch("https://filemv.com/api/files/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "name": "report-final.pdf",
    "parentId": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091"
  }),
});
const data = await res.json();
Response200application/json
{
  "commentsCount": 1,
  "contentKind": "FILE",
  "deletedAt": "2026-10-04T09:30:00Z",
  "downloadCount": 1,
  "id": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
  "likesCount": 1,
  "mimeType": "string",
  "name": "vacation.jpg",
  "parentId": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
  "previewUrl": "https://filemv.com/…",
  "shareMode": "string",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "size": 1048576,
  "type": "file",
  "uploadedAt": "2026-10-04T09:30:00Z"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

DELETE/api/files/{id}API key or session

Soft-delete an item (move it to Trash)

Folders are soft-deleted along with every descendant. Items can be restored via POST /{id}/restore until the TrashCleanupService sweeps them after the retention window.

Parameters

NameInTypeDescription
id requiredpathstringUUID of the item to soft-delete

Responses

  • 204Item soft-deleted
  • 401Missing or invalid Bearer token
  • 404Item is unknown or owned by a different user

DELETE/api/files/{id}

const res = await fetch("https://filemv.com/api/files/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b", {
  method: "DELETE",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
console.log(res.status); // 204
Response204

No response body.

Try it

This endpoint changes data — run it from your own environment with the sample above.

GET/api/files/{id}/pathAPI key or session

Return the ancestor chain of an item

Used by the Drive UI to render breadcrumbs when a user deep-links into a folder. The returned list is ordered root-first and ends with the item itself.

Parameters

NameInTypeDescription
id requiredpathstringUUID of the item whose ancestors should be returned

Responses

  • 200Ordered ancestor list, ending with the item itself
  • 401Missing or invalid Bearer token
  • 404Item is unknown or owned by a different user

GET/api/files/{id}/path

const res = await fetch("https://filemv.com/api/files/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b/path", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
const data = await res.json();
Response200application/json
[
  {
    "commentsCount": 1,
    "contentKind": "FILE",
    "deletedAt": "2026-10-04T09:30:00Z",
    "downloadCount": 1,
    "id": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
    "likesCount": 1,
    "mimeType": "string",
    "name": "vacation.jpg",
    "parentId": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
    "previewUrl": "https://filemv.com/…",
    "shareMode": "string",
    "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
    "size": 1048576,
    "type": "file",
    "uploadedAt": "2026-10-04T09:30:00Z"
  }
]
Try it

This call needs parameters — run it from your own environment with the sample above.

DELETE/api/files/{id}/permanentAPI key or session

Permanently delete a soft-deleted item

Deletes the stored file and its metadata for good. Idempotent — calling on an already-purged id still returns 204. The item must be in Trash first; calling on an alive row returns 409.

Parameters

NameInTypeDescription
id requiredpathstringUUID of the soft-deleted item to permanently delete

Responses

  • 204Item permanently deleted (or already purged)
  • 401Missing or invalid Bearer token
  • 404Item is unknown or owned by a different user
  • 409Item exists but is not soft-deleted — caller must soft-delete first

DELETE/api/files/{id}/permanent

const res = await fetch("https://filemv.com/api/files/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b/permanent", {
  method: "DELETE",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
console.log(res.status); // 204
Response204

No response body.

Try it

This endpoint changes data — run it from your own environment with the sample above.

POST/api/files/{id}/restoreAPI key or session

Restore a soft-deleted item from Trash

If the original parent folder is itself in Trash the item is restored to the Drive root. Restoring a folder also restores every descendant that was trashed together with it (same delete); descendants trashed separately before the folder stay in Trash.

Parameters

NameInTypeDescription
id requiredpathstringUUID of the soft-deleted item to restore

Responses

  • 200The restored item
  • 401Missing or invalid Bearer token
  • 404Item is unknown, owned by a different user, or not in Trash
  • 409A name collision in the restore destination blocks the restore

POST/api/files/{id}/restore

const res = await fetch("https://filemv.com/api/files/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b/restore", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
const data = await res.json();
Response200application/json
{
  "commentsCount": 1,
  "contentKind": "FILE",
  "deletedAt": "2026-10-04T09:30:00Z",
  "downloadCount": 1,
  "id": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
  "likesCount": 1,
  "mimeType": "string",
  "name": "vacation.jpg",
  "parentId": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
  "previewUrl": "https://filemv.com/…",
  "shareMode": "string",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "size": 1048576,
  "type": "file",
  "uploadedAt": "2026-10-04T09:30:00Z"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

GET/api/files/{fileId}/download-urlAPI key or session

Get a signed download URL for a caller-owned file

Inline=true returns a URL with Content-Disposition: inline (browser preview); inline=false returns an attachment URL. A file the caller owns that is quarantined (under takedown) is still served to its owner, from the quarantine store.

Parameters

NameInTypeDescription
fileId requiredpathstring
inline querybooleantrue to request an inline-disposition URL (preview); false (default) yields an attachment URL.

Responses

  • 200Body carries the signed download URL
  • 401Missing or invalid Bearer token
  • 403Caller does not own the file
  • 404File not found
  • 410file_purged — the file's bytes were purged (the row is kept); no URL is minted

GET/api/files/{fileId}/download-url

const res = await fetch("https://filemv.com/api/files/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b/download-url", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
const data = await res.json();
Response200application/json
{
  "downloadUrl": "https://filemv.com/…"
}
Try it

This call needs parameters — run it from your own environment with the sample above.

POST/api/files/archiveAPI key or session

Create a one-shot ZIP for a selection of caller-owned files

Verifies ownership server-side for every id in the request and builds the archive. Returns a one-shot download URL for the ZIP on success.

Request bodyapplication/json

  • fileIdsstring[]required Caller-owned file UUIDs to bundle into the ZIP. Bounded [1, 200] — both limits surface as 400.

Responses

  • 200Manifest accepted — body carries the archive download URL
  • 400Validation failed (empty / oversize fileIds list)
  • 401Missing or invalid Bearer token
  • 403One or more ids are unknown, purged, or owned by a different user
  • 413Selection exceeds the per-archive size cap (storage.archive.max-bytes) — the uniform error envelope with code=archive_size_exceeded, plus actual_bytes / cap_bytes
  • 502Storage worker rejected the archive request for a reason other than the size cap

POST/api/files/archive

const res = await fetch("https://filemv.com/api/files/archive", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "fileIds": [
      "6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b",
      "9a8b7c6d-5e4f-4321-9876-0fedcba98765"
    ]
  }),
});
const data = await res.json();
Response200application/json
{}
Try it

This endpoint changes data — run it from your own environment with the sample above.

Sharing

POST/api/files/share-multiAPI key or session

Mint a single share token spanning multiple caller-owned files

Requires 2..50 distinct ids, all owned, not purged, and non-anonymous. The whole call is rejected on the first failed id — no partial group is committed.

Request bodyapplication/json

  • expiresInDaysinteger Optional share-link expiry in days from now, applied to every file in the group. null or 0 = never expires; 1..365 = expire after N days.
  • fileIdsstring[]required Caller-owned file UUIDs to bundle under one share token. Bounded [2, 50] — single-file shares should go through POST /api/files/{id}/share instead.
  • notifyOnDownloadboolean When true, the file owner receives an email notification when an emailed recipient downloads any file in this multi-share group. Omit or set to false to disable.
  • notifyOnViewboolean Optional. When true, the file owner receives an email notification when an emailed recipient first opens any file in this multi-share group. Distinct from notifyOnDownload: fires on view.
  • notifyPerRecipientboolean Optional. When true, switches to per-recipient notification mode: the owner receives one email per recipient download instead of only the first. Requires notifyOnDownload=true to have any visible effect.
  • passwordstring Optional share password (4 characters to 72 bytes of UTF-8), applied to every file in the group. null/omitted = no password;
  • shareModestring Optional target share mode (SHARED | PUBLIC). Defaults to SHARED — link-only, unlisted. PRIVATE · PUBLIC · SHARED

Responses

  • 201Shared token + frontend URL + final file count
  • 400Validation failed (size out of [2,50], duplicate ids, invalid shareMode)
  • 401Missing or invalid Bearer token
  • 403Caller does not own one or more files
  • 404One or more files not found
  • 409One or more files are anonymous

POST/api/files/share-multi

const res = await fetch("https://filemv.com/api/files/share-multi", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "expiresInDays": 7,
    "fileIds": [
      "6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b",
      "9a8b7c6d-5e4f-4321-9876-0fedcba98765"
    ],
    "notifyOnDownload": true,
    "shareMode": "SHARED"
  }),
});
const data = await res.json();
Response201application/json
{
  "fileCount": 1,
  "shareMode": "string",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "shareUrl": "https://filemv.com/…"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

PATCH/api/files/visibilityAPI key or session

Toggle PUBLIC / PRIVATE visibility for a batch of items

Folder ids are accepted and expand to their direct file children (depth-1). Per-item failures are surfaced inline so a partially-failed batch never aborts the whole call. Returns 200 when every item succeeds and 207 Multi-Status when at least one item fails. Inline per-id codes: 404 unknown, 409 trashed-or-anonymous, 410 purged.

Request bodyapplication/json

  • idsstring[]required Mix of file ids and folder ids to retarget. Folder ids cascade to direct file children only (depth-1). Bounded [1, 200] — caps the per-request work (one SELECT per id in FileVisibilityService).
  • modestringrequired Target visibility. Restricted to PUBLIC (mint/keep share token + list site-wide) or PRIVATE (clear token + unlist). SHARED goes through the per-file PATCH. PRIVATE · PUBLIC · SHARED

Responses

  • 200Every item succeeded — `updated` populated, `failed` empty
  • 207Partial failure — `failed` is non-empty (per-id 404 / 409 / 410); `updated` holds the items that did succeed
  • 400Validation failed (empty ids, invalid mode)
  • 401Missing or invalid Bearer token

PATCH/api/files/visibility

const res = await fetch("https://filemv.com/api/files/visibility", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "ids": [
      "6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b",
      "9a8b7c6d-5e4f-4321-9876-0fedcba98765"
    ],
    "mode": "PUBLIC"
  }),
});
const data = await res.json();
Response200application/json
{
  "failed": [
    {
      "id": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
      "message": "string",
      "status": 1
    }
  ],
  "updated": [
    {
      "commentsCount": 1,
      "contentKind": "FILE",
      "deletedAt": "2026-10-04T09:30:00Z",
      "downloadCount": 1,
      "id": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
      "likesCount": 1,
      "mimeType": "string",
      "name": "vacation.jpg",
      "parentId": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
      "previewUrl": "https://filemv.com/…",
      "shareMode": "string",
      "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
      "size": 1048576,
      "type": "file",
      "uploadedAt": "2026-10-04T09:30:00Z"
    }
  ]
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

POST/api/files/{fileId}/shareAPI key or session

Mint or return the active share for a caller-owned file

Idempotent — if an active share exists, returns it instead of minting a new one. On an existing share only the fields present in the body are applied; an omitted field (mode, expiresInDays, notify flags, password) keeps its current value. expiresInDays=0 clears the expiry. An already-lapsed expiry is dropped when no new one is sent.

Parameters

NameInTypeDescription
fileId requiredpathstring

Request bodyapplication/json

  • expiresInDaysinteger Optional share-link expiry in days from now. 0 = never expires; 1..365 = expire after N days. Omitted: never on a new share, unchanged on an existing one (an already-lapsed expiry is dropped).
  • notifyOnDownloadboolean When true, the owner receives an email notification when an emailed recipient downloads this share. Omitted: false on a new share, unchanged on an existing one.
  • notifyOnViewboolean Optional. When true, the owner receives an email notification when an emailed recipient first opens this share link. Distinct from notifyOnDownload: fires on open (view), not download. Omitted: false on a new share, unchanged on an existing one.
  • notifyPerRecipientboolean Optional. When true, switches to per-recipient notification mode: the owner receives one email per recipient download instead of only the first. Requires notifyOnDownload=true to have any visible effect. Omitted: false on a new share, unchanged on an existing one.
  • passwordstring Optional share password (4 characters to 72 bytes of UTF-8). null/omitted = no password on a new share, unchanged on an existing one (clear it via PATCH …/share/password); empty/1–3 chars or over 72 bytes = 400.
  • shareModestring Optional target share mode (SHARED | PUBLIC). Omitted: SHARED (link-only, unlisted) on a new share, unchanged on an existing one. PRIVATE · PUBLIC · SHARED

Responses

  • 200Active share token + frontend share path
  • 400Validation failed (invalid shareMode)
  • 401Missing or invalid Bearer token
  • 403Caller does not own the file
  • 404File not found

POST/api/files/{fileId}/share

const res = await fetch("https://filemv.com/api/files/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b/share", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "expiresInDays": 7,
    "notifyOnDownload": true,
    "password": "correct-horse-42",
    "shareMode": "SHARED"
  }),
});
const data = await res.json();
Response200application/json
{
  "expiresAt": "2026-10-04T09:30:00Z",
  "notifyOnView": true,
  "notifyOwnerOnDownload": true,
  "notifyPerRecipient": true,
  "passwordProtected": true,
  "shareMode": "string",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "shareUrl": "https://filemv.com/…",
  "shortUrl": "https://filemv.com/…"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

PATCH/api/files/{fileId}/shareAPI key or session

Update an existing share's mode without rotating its token

Re-issues the same share token under a different mode. The token URL stays stable, only its visibility changes.

Parameters

NameInTypeDescription
fileId requiredpathstring

Request bodyapplication/json

  • shareModestringrequired New share mode (PRIVATE | SHARED | PUBLIC). Required. PRIVATE · PUBLIC · SHARED

Responses

  • 200Updated share descriptor
  • 400Validation failed (missing/invalid shareMode)
  • 401Missing or invalid Bearer token
  • 403Caller does not own the file
  • 404File or active share not found

PATCH/api/files/{fileId}/share

const res = await fetch("https://filemv.com/api/files/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b/share", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "shareMode": "SHARED"
  }),
});
const data = await res.json();
Response200application/json
{
  "expiresAt": "2026-10-04T09:30:00Z",
  "notifyOnView": true,
  "notifyOwnerOnDownload": true,
  "notifyPerRecipient": true,
  "passwordProtected": true,
  "shareMode": "string",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "shareUrl": "https://filemv.com/…",
  "shortUrl": "https://filemv.com/…"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

DELETE/api/files/{fileId}/shareAPI key or session

Revoke a file's active share

Idempotent — returns 204 whether or not a share token existed for the caller's file. The 404 is unrelated to the share token: it is returned only when the file itself does not exist.

Parameters

NameInTypeDescription
fileId requiredpathstring

Responses

  • 204Share revoked (or no-op if none existed)
  • 401Missing or invalid Bearer token
  • 403Caller does not own the file
  • 404File not found (the file itself is absent, not the share token)

DELETE/api/files/{fileId}/share

const res = await fetch("https://filemv.com/api/files/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b/share", {
  method: "DELETE",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
console.log(res.status); // 204
Response204

No response body.

Try it

This endpoint changes data — run it from your own environment with the sample above.

PATCH/api/files/{fileId}/share/expiryAPI key or session

Update an existing share's expiry without rotating its token

Sets or clears the share-link expiry. expiresInDays=0 or null removes the expiry. Applied to all rows sharing the same token (multi-file share support).

Parameters

NameInTypeDescription
fileId requiredpathstring

Request bodyapplication/json

  • expiresInDaysinteger Days from now until the share expires. 0 or null = never expires. Server enforces the configured max-days cap via ShareExpiryResolver.

Responses

  • 200Updated share descriptor with new expiry
  • 400Validation failed (expiresInDays out of [0,365] or exceeds configured max-days)
  • 401Missing or invalid Bearer token
  • 403Caller does not own the file
  • 404File not found
  • 409No active share — mint one first via POST

PATCH/api/files/{fileId}/share/expiry

const res = await fetch("https://filemv.com/api/files/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b/share/expiry", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "expiresInDays": 30
  }),
});
const data = await res.json();
Response200application/json
{
  "expiresAt": "2026-10-04T09:30:00Z",
  "notifyOnView": true,
  "notifyOwnerOnDownload": true,
  "notifyPerRecipient": true,
  "passwordProtected": true,
  "shareMode": "string",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "shareUrl": "https://filemv.com/…",
  "shortUrl": "https://filemv.com/…"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

PATCH/api/files/{fileId}/share/notifyAPI key or session

Enable or disable download-notification emails for a file share

Sets or clears the notify-on-download flag on an existing share without rotating its token. Applied to all rows sharing the same token (multi-file share support). notifyOnDownload=true enables owner email notifications on recipient download.

Parameters

NameInTypeDescription
fileId requiredpathstring

Request bodyapplication/json

  • notifyOnDownloadbooleanrequired true to receive an email when an emailed recipient downloads; false to disable.
  • notifyOnViewboolean Optional. true = notify the owner when an emailed recipient first opens this share link; false = disable view notifications; null = leave the existing setting unchanged.
  • notifyPerRecipientboolean Optional. true = one notification per recipient download; false = one notification per share (first-download-only mode); null = leave the existing per-recipient setting unchanged.

Responses

  • 200Updated share descriptor with the new notifyOwnerOnDownload flag
  • 400Validation failed (notifyOnDownload is null)
  • 401Missing or invalid Bearer token
  • 403Caller does not own the file
  • 404File not found
  • 409No active share — mint one first via POST

PATCH/api/files/{fileId}/share/notify

const res = await fetch("https://filemv.com/api/files/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b/share/notify", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "notifyOnDownload": true
  }),
});
const data = await res.json();
Response200application/json
{
  "expiresAt": "2026-10-04T09:30:00Z",
  "notifyOnView": true,
  "notifyOwnerOnDownload": true,
  "notifyPerRecipient": true,
  "passwordProtected": true,
  "shareMode": "string",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "shareUrl": "https://filemv.com/…",
  "shortUrl": "https://filemv.com/…"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

PATCH/api/files/{fileId}/share/passwordAPI key or session

Set, change, or clear the password on an existing file share

Updates the BCrypt-hashed password on an existing share without rotating its token. Applied to all rows sharing the same token (multi-file share support). Pass password=null to clear the password (remove protection). The raw password is never logged or returned.

Parameters

NameInTypeDescription
fileId requiredpathstring

Request bodyapplication/json

  • passwordstring Plaintext password (4 characters to 72 bytes of UTF-8). null = clear the password (no password protection).

Responses

  • 200Updated share descriptor with the new passwordProtected flag
  • 400Validation failed (password shorter than 4 chars or longer than 128 chars)
  • 401Missing or invalid Bearer token
  • 403Caller does not own the file
  • 404File not found
  • 409No active share — mint one first via POST

PATCH/api/files/{fileId}/share/password

const res = await fetch("https://filemv.com/api/files/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b/share/password", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "password": "correct-horse-42"
  }),
});
const data = await res.json();
Response200application/json
{
  "expiresAt": "2026-10-04T09:30:00Z",
  "notifyOnView": true,
  "notifyOwnerOnDownload": true,
  "notifyPerRecipient": true,
  "passwordProtected": true,
  "shareMode": "string",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "shareUrl": "https://filemv.com/…",
  "shortUrl": "https://filemv.com/…"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

PATCH/api/files/{fileId}/share/slugAPI key or session

Set, change, or clear the custom vanity slug on an existing file share

Sets or clears a custom vanity short link for the share. Available on every plan. Pass slug=null to revert to the auto-generated short link. The slug must be 3–30 chars, lowercase alphanumeric/hyphens, no leading/trailing/consecutive hyphens, and must not be a reserved or profane term.

Parameters

NameInTypeDescription
fileId requiredpathstring

Request bodyapplication/json

  • slugstring Custom vanity slug (3–30 chars, lowercase letters/digits/hyphens). null or blank = clear the vanity slug (revert to auto-generated short link).

Responses

  • 200Updated share descriptor with the new shortUrl
  • 400Slug fails format, reserved-word, or profanity validation
  • 401Missing or invalid Bearer token
  • 403Caller does not own the file
  • 404File not found
  • 409No active share, or slug is already taken by another share

PATCH/api/files/{fileId}/share/slug

const res = await fetch("https://filemv.com/api/files/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b/share/slug", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "slug": "summer-photos"
  }),
});
const data = await res.json();
Response200application/json
{
  "expiresAt": "2026-10-04T09:30:00Z",
  "notifyOnView": true,
  "notifyOwnerOnDownload": true,
  "notifyPerRecipient": true,
  "passwordProtected": true,
  "shareMode": "string",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "shareUrl": "https://filemv.com/…",
  "shortUrl": "https://filemv.com/…"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

GET/api/files/{fileId}/share/unlock-eventsAPI key or session

Get unlock-attempt audit log for a file share

Owner-only. Returns up to 20 recent unlock attempts (timestamp + result), the 24-hour failed-attempt count, and a brute-force alert flag. IP hashes are never exposed. Returns an empty view when no share is active or the share has no password-attempt history.

Parameters

NameInTypeDescription
fileId requiredpathstring

Responses

  • 200Audit view
  • 401Missing or invalid Bearer token
  • 403Caller does not own the file
  • 404File not found

GET/api/files/{fileId}/share/unlock-events

const res = await fetch("https://filemv.com/api/files/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b/share/unlock-events", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
const data = await res.json();
Response200application/json
{
  "bruteForceAlert": true,
  "failedLast24h": 1,
  "recentEvents": [
    {
      "createdAt": "2026-10-04T09:30:00Z",
      "result": "string"
    }
  ]
}
Try it

This call needs parameters — run it from your own environment with the sample above.

POST/api/folders/{folderId}/shareAPI key or session

Mint or return the active share for a caller-owned folder

Idempotent — returns the existing active share if one is present. On an existing share only the fields present in the body are applied; an omitted field (mode, expiresInDays, notify flags, password) keeps its current value. expiresInDays=0 clears the expiry. An already-lapsed expiry is dropped when no new one is sent.

Parameters

NameInTypeDescription
folderId requiredpathstring

Request bodyapplication/json

  • expiresInDaysinteger Optional share-link expiry in days from now. 0 = never expires; 1..365 = expire after N days. Omitted: never on a new share, unchanged on an existing one (an already-lapsed expiry is dropped).
  • notifyOnDownloadboolean When true, the owner receives an email notification when an emailed recipient downloads this share. Omitted: false on a new share, unchanged on an existing one.
  • notifyOnViewboolean Optional. When true, the owner receives an email notification when an emailed recipient first opens this share link. Distinct from notifyOnDownload: fires on open (view), not download. Omitted: false on a new share, unchanged on an existing one.
  • notifyPerRecipientboolean Optional. When true, switches to per-recipient notification mode: the owner receives one email per recipient download instead of only the first. Requires notifyOnDownload=true to have any visible effect. Omitted: false on a new share, unchanged on an existing one.
  • passwordstring Optional share password (4 characters to 72 bytes of UTF-8). null/omitted = no password on a new share, unchanged on an existing one (clear it via PATCH …/share/password); empty/1–3 chars or over 72 bytes = 400.
  • shareModestring Optional target share mode (SHARED | PUBLIC). Omitted: SHARED (link-only, unlisted) on a new share, unchanged on an existing one. PRIVATE · PUBLIC · SHARED

Responses

  • 200Active share token + frontend share path
  • 400Validation failed (invalid shareMode)
  • 401Missing or invalid Bearer token
  • 403Caller does not own the folder
  • 404Folder not found

POST/api/folders/{folderId}/share

const res = await fetch("https://filemv.com/api/folders/2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091/share", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "expiresInDays": 7,
    "notifyOnDownload": true,
    "password": "correct-horse-42",
    "shareMode": "SHARED"
  }),
});
const data = await res.json();
Response200application/json
{
  "expiresAt": "2026-10-04T09:30:00Z",
  "notifyOnView": true,
  "notifyOwnerOnDownload": true,
  "notifyPerRecipient": true,
  "passwordProtected": true,
  "shareMode": "string",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "shareUrl": "https://filemv.com/…",
  "shortUrl": "https://filemv.com/…"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

PATCH/api/folders/{folderId}/shareAPI key or session

Update a folder share's mode without rotating its token

Re-issues the same share token under a different mode. The token URL stays stable, only its visibility changes.

Parameters

NameInTypeDescription
folderId requiredpathstring

Request bodyapplication/json

  • shareModestringrequired New share mode (PRIVATE | SHARED | PUBLIC). Required. PRIVATE · PUBLIC · SHARED

Responses

  • 200Updated share descriptor
  • 400Validation failed (missing/invalid shareMode)
  • 401Missing or invalid Bearer token
  • 403Caller does not own the folder
  • 404Folder or active share not found

PATCH/api/folders/{folderId}/share

const res = await fetch("https://filemv.com/api/folders/2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091/share", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "shareMode": "SHARED"
  }),
});
const data = await res.json();
Response200application/json
{
  "expiresAt": "2026-10-04T09:30:00Z",
  "notifyOnView": true,
  "notifyOwnerOnDownload": true,
  "notifyPerRecipient": true,
  "passwordProtected": true,
  "shareMode": "string",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "shareUrl": "https://filemv.com/…",
  "shortUrl": "https://filemv.com/…"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

DELETE/api/folders/{folderId}/shareAPI key or session

Revoke a folder's active share

Idempotent — returns 204 whether or not a share token existed for the caller's folder. The 404 is unrelated to the share token: it is returned only when the folder itself does not exist.

Parameters

NameInTypeDescription
folderId requiredpathstring

Responses

  • 204Share revoked (or no-op if none existed)
  • 401Missing or invalid Bearer token
  • 403Caller does not own the folder
  • 404Folder not found (the folder itself is absent, not the share token)

DELETE/api/folders/{folderId}/share

const res = await fetch("https://filemv.com/api/folders/2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091/share", {
  method: "DELETE",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
console.log(res.status); // 204
Response204

No response body.

Try it

This endpoint changes data — run it from your own environment with the sample above.

PATCH/api/folders/{folderId}/share/expiryAPI key or session

Update a folder share's expiry without rotating its token

Sets or clears the share-link expiry. expiresInDays=0 or null removes the expiry. Requires an active folder share.

Parameters

NameInTypeDescription
folderId requiredpathstring

Request bodyapplication/json

  • expiresInDaysinteger Days from now until the share expires. 0 or null = never expires. Server enforces the configured max-days cap via ShareExpiryResolver.

Responses

  • 200Updated share descriptor with new expiry
  • 400Validation failed (expiresInDays out of [0,365] or exceeds configured max-days)
  • 401Missing or invalid Bearer token
  • 403Caller does not own the folder
  • 404Folder not found
  • 409No active share — mint one first via POST

PATCH/api/folders/{folderId}/share/expiry

const res = await fetch("https://filemv.com/api/folders/2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091/share/expiry", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "expiresInDays": 30
  }),
});
const data = await res.json();
Response200application/json
{
  "expiresAt": "2026-10-04T09:30:00Z",
  "notifyOnView": true,
  "notifyOwnerOnDownload": true,
  "notifyPerRecipient": true,
  "passwordProtected": true,
  "shareMode": "string",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "shareUrl": "https://filemv.com/…",
  "shortUrl": "https://filemv.com/…"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

PATCH/api/folders/{folderId}/share/notifyAPI key or session

Enable or disable download-notification emails for a folder share

Sets or clears the notify-on-download flag on an existing folder share without rotating its token. notifyOnDownload=true enables owner email notifications on recipient download.

Parameters

NameInTypeDescription
folderId requiredpathstring

Request bodyapplication/json

  • notifyOnDownloadbooleanrequired true to receive an email when an emailed recipient downloads; false to disable.
  • notifyOnViewboolean Optional. true = notify the owner when an emailed recipient first opens this share link; false = disable view notifications; null = leave the existing setting unchanged.
  • notifyPerRecipientboolean Optional. true = one notification per recipient download; false = one notification per share (first-download-only mode); null = leave the existing per-recipient setting unchanged.

Responses

  • 200Updated share descriptor with the new notifyOwnerOnDownload flag
  • 400Validation failed (notifyOnDownload is null)
  • 401Missing or invalid Bearer token
  • 403Caller does not own the folder
  • 404Folder not found
  • 409No active share — mint one first via POST

PATCH/api/folders/{folderId}/share/notify

const res = await fetch("https://filemv.com/api/folders/2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091/share/notify", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "notifyOnDownload": true
  }),
});
const data = await res.json();
Response200application/json
{
  "expiresAt": "2026-10-04T09:30:00Z",
  "notifyOnView": true,
  "notifyOwnerOnDownload": true,
  "notifyPerRecipient": true,
  "passwordProtected": true,
  "shareMode": "string",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "shareUrl": "https://filemv.com/…",
  "shortUrl": "https://filemv.com/…"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

PATCH/api/folders/{folderId}/share/passwordAPI key or session

Set, change, or clear the password on an existing folder share

Updates the BCrypt-hashed password on an existing folder share without rotating its token. Pass password=null to clear the password (remove protection). The raw password is never logged or returned.

Parameters

NameInTypeDescription
folderId requiredpathstring

Request bodyapplication/json

  • passwordstring Plaintext password (4 characters to 72 bytes of UTF-8). null = clear the password (no password protection).

Responses

  • 200Updated share descriptor with the new passwordProtected flag
  • 400Validation failed (password shorter than 4 chars or longer than 128 chars)
  • 401Missing or invalid Bearer token
  • 403Caller does not own the folder
  • 404Folder not found
  • 409No active share — mint one first via POST

PATCH/api/folders/{folderId}/share/password

const res = await fetch("https://filemv.com/api/folders/2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091/share/password", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "password": "correct-horse-42"
  }),
});
const data = await res.json();
Response200application/json
{
  "expiresAt": "2026-10-04T09:30:00Z",
  "notifyOnView": true,
  "notifyOwnerOnDownload": true,
  "notifyPerRecipient": true,
  "passwordProtected": true,
  "shareMode": "string",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "shareUrl": "https://filemv.com/…",
  "shortUrl": "https://filemv.com/…"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

PATCH/api/folders/{folderId}/share/proofingAPI key or session

Enable or disable client-delivery proofing for a folder share

Sets or clears the proofing-enabled flag on an existing folder share without rotating its token. proofingEnabled=true lets emailed recipients star/select files and leave notes.

Parameters

NameInTypeDescription
folderId requiredpathstring

Request bodyapplication/json

  • proofingEnabledbooleanrequired true to enable client-delivery proofing (star/select + notes) on this folder share; false to disable.

Responses

  • 200Updated share descriptor
  • 400Validation failed (proofingEnabled is null)
  • 401Missing or invalid Bearer token
  • 403Caller does not own the folder
  • 404Folder not found
  • 409No active share — mint one first via POST

PATCH/api/folders/{folderId}/share/proofing

const res = await fetch("https://filemv.com/api/folders/2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091/share/proofing", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "proofingEnabled": true
  }),
});
const data = await res.json();
Response200application/json
{
  "expiresAt": "2026-10-04T09:30:00Z",
  "notifyOnView": true,
  "notifyOwnerOnDownload": true,
  "notifyPerRecipient": true,
  "passwordProtected": true,
  "shareMode": "string",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "shareUrl": "https://filemv.com/…",
  "shortUrl": "https://filemv.com/…"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

PATCH/api/folders/{folderId}/share/slugAPI key or session

Set, change, or clear the custom vanity slug on an existing folder share

Sets or clears a custom vanity short link for the folder share. Available on every plan. Pass slug=null to revert to the auto-generated short link. The slug must be 3–30 chars, lowercase alphanumeric/hyphens, no leading/trailing/consecutive hyphens, and must not be a reserved or profane term.

Parameters

NameInTypeDescription
folderId requiredpathstring

Request bodyapplication/json

  • slugstring Custom vanity slug (3–30 chars, lowercase letters/digits/hyphens). null or blank = clear the vanity slug (revert to auto-generated short link).

Responses

  • 200Updated share descriptor with the new shortUrl
  • 400Slug fails format, reserved-word, or profanity validation
  • 401Missing or invalid Bearer token
  • 403Caller does not own the folder
  • 404Folder not found
  • 409No active share, or slug is already taken by another share

PATCH/api/folders/{folderId}/share/slug

const res = await fetch("https://filemv.com/api/folders/2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091/share/slug", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "slug": "summer-photos"
  }),
});
const data = await res.json();
Response200application/json
{
  "expiresAt": "2026-10-04T09:30:00Z",
  "notifyOnView": true,
  "notifyOwnerOnDownload": true,
  "notifyPerRecipient": true,
  "passwordProtected": true,
  "shareMode": "string",
  "shareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "shareUrl": "https://filemv.com/…",
  "shortUrl": "https://filemv.com/…"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

GET/api/folders/{folderId}/share/unlock-eventsAPI key or session

Get unlock-attempt audit log for a folder share

Owner-only. Returns up to 20 recent unlock attempts (timestamp + result), the 24-hour failed-attempt count, and a brute-force alert flag. IP hashes are never exposed. Returns an empty view when no share is active or the share has no password-attempt history.

Parameters

NameInTypeDescription
folderId requiredpathstring

Responses

  • 200Audit view
  • 401Missing or invalid Bearer token
  • 403Caller does not own the folder
  • 404Folder not found

GET/api/folders/{folderId}/share/unlock-events

const res = await fetch("https://filemv.com/api/folders/2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091/share/unlock-events", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
const data = await res.json();
Response200application/json
{
  "bruteForceAlert": true,
  "failedLast24h": 1,
  "recentEvents": [
    {
      "createdAt": "2026-10-04T09:30:00Z",
      "result": "string"
    }
  ]
}
Try it

This call needs parameters — run it from your own environment with the sample above.

GET/api/share/slug-availableAPI key or session

Check vanity-slug availability

Returns {available:true} when the slug is free. Returns {available:false, reason:...} for taken/reserved/profane/invalid. Advisory only — does not reserve the slug.

Parameters

NameInTypeDescription
slug requiredquerystring

Responses

  • 200Availability result
  • 401Missing or invalid JWT

GET/api/share/slug-available

const res = await fetch("https://filemv.com/api/share/slug-available?slug=summer-photos", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
const data = await res.json();
Response200*/*

No response body.

Try it

This call needs parameters — run it from your own environment with the sample above.

Public

GET/api/public/share/{token}public

Resolve a share token to its public payload

Public, unauthenticated read. The token may be a file share's or a folder share's token, or a custom slug. For folder shares the optional folderId query parameter narrows the response to that subfolder's direct contents under the share's subtree. When the share is password-protected and no valid X-Share-Unlock token is provided, returns 200 with passwordRequired=true and an empty content envelope.

Parameters

NameInTypeDescription
token requiredpathstringRecipient-facing share token returned by the share-mint endpoints
folderId querystringOptional subfolder id to navigate into on a folder share. Must live inside the share's subtree.
r querystringOptional per-recipient tracking token from an emailed share link. Best-effort: unknown/foreign values are silently ignored.

Responses

  • 200Share payload (full content) OR locked stub (passwordRequired=true when password-protected and no valid unlock token)
  • 400folderId refers to a non-folder item on a folder share
  • 404No share matches the token, or folderId is outside the share's subtree
  • 410Every file behind this token has been deleted or has expired, or the share link's expiry has passed
  • 429Too many resolves from this IP — Retry-After header indicates the backoff window

GET/api/public/share/{token}

const res = await fetch("https://filemv.com/api/public/share/Qm4t7Xz2Lp9Kd3Wf8Rb1Nc", {
  method: "GET",
});
const data = await res.json();
Response200application/json
{
  "adsEnabled": true,
  "archiveDownloadUrl": "https://filemv.com/…",
  "canonicalSlug": "string",
  "containingFolderShareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
  "expiresAt": "2026-10-04T09:30:00Z",
  "files": [
    {
      "contentType": "string",
      "displayUrl": "https://filemv.com/…",
      "downloadUrl": "https://filemv.com/…",
      "exif": {
        "gpsPresent": true,
        "groups": [
          null
        ],
        "summary": [
          "1/500 s",
          "2026. 08. 12.",
          "35 mm",
          "6000×4000",
          "ISO 200",
          "Sony α7 IV",
          "ƒ/2.8"
        ]
      },
      "fileName": "vacation.jpg",
      "id": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
      "inlineUrl": "https://filemv.com/…",
      "paste": {
        "charCount": 1,
        "language": "string",
        "lineCount": 1,
        "rawUrl": "https://filemv.com/…",
        "revision": 1,
        "updatedAt": "2026-10-04T09:30:00Z"
      },
      "sizeBytes": 1048576
    }
  ],
  …
Try it

This call needs parameters — run it from your own environment with the sample above.

GET/api/public/share/{token}/archivepublic

Stream a folder share as a single ZIP archive

Builds a manifest of every live FILE descendant under the requested folder (root by default) and redirects the recipient to a one-shot, short-lived streaming-ZIP URL. File / multi-file shares are not eligible; they fall back to the per-file client-side fan-out.

Parameters

NameInTypeDescription
token requiredpathstringRecipient-facing share token — must resolve to a folder share
folderId querystringOptional subfolder id (under the share's subtree) to scope the archive to. Defaults to the share root.
u querystringUnlock token proving the caller supplied the correct password (query-param form). Used for browser-navigation downloads where a custom header cannot be set.

Responses

  • 302Redirect to the one-shot streaming-ZIP URL
  • 400Share is not a folder share, or folderId refers to a non-folder item, or the manifest fails validation
  • 404Unknown share token, or folderId is outside the share's subtree
  • 413The folder exceeds the per-archive size cap; the uniform error envelope with code=archive_size_exceeded, plus actual_bytes / cap_bytes

GET/api/public/share/{token}/archive

const res = await fetch("https://filemv.com/api/public/share/Qm4t7Xz2Lp9Kd3Wf8Rb1Nc/archive", {
  method: "GET",
});
const data = await res.json();
Try it

This call needs parameters — run it from your own environment with the sample above.

GET/api/public/share/{token}/files/{fileId}/urlspublic

Mint fresh download URLs for one file of a share (media token refresh)

Long media playback outlives the 10-minute download-token TTL; the player calls this to swap in fresh tokenised URLs without re-resolving the whole share (which would record a share-view event). Applies the same expiry / password / membership gates as the share resolution itself. Records NO view/open analytics.

Parameters

NameInTypeDescription
token requiredpathstringRecipient-facing share token or vanity slug
fileId requiredpathstringId of the file whose URLs should be re-minted; must belong to the share
r querystringOptional per-recipient tracking token — preserved as an attribution claim in the minted download token. Foreign/unknown values are silently dropped.

Responses

  • 200Fresh inline + attachment URLs
  • 401Share is password-protected and no valid X-Share-Unlock token was supplied
  • 404No share matches the token, or the file does not belong to the share
  • 410The share's link expiry has passed
  • 429Too many refreshes from this IP — Retry-After header indicates the backoff window

GET/api/public/share/{token}/files/{fileId}/urls

const res = await fetch("https://filemv.com/api/public/share/Qm4t7Xz2Lp9Kd3Wf8Rb1Nc/files/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b/urls", {
  method: "GET",
});
const data = await res.json();
Response200application/json
{
  "displayUrl": "https://filemv.com/…",
  "downloadUrl": "https://filemv.com/…",
  "inlineUrl": "https://filemv.com/…"
}
Try it

This call needs parameters — run it from your own environment with the sample above.

POST/api/public/share/{token}/unlockpublic

Verify a share password and obtain a short-lived unlock token

Verifies the supplied password against the share's BCrypt hash. On success returns a signed unlock token (and the full share payload) that the client should send as X-Share-Unlock on subsequent GET requests. Wrong passwords and rate-limit exhaustion are surfaced as 401 / 429.

Parameters

NameInTypeDescription
token requiredpathstringThe share token to unlock

Request bodyapplication/json

  • passwordstring The share password
  • recTokenstring Optional per-recipient token from an emailed link

Responses

  • 200Password correct — body carries the unlock token and the full share payload
  • 400The share is not password-protected
  • 401Incorrect password (action=share_password_invalid)
  • 404Share token not found
  • 410Share has expired
  • 429Too many failed attempts — Retry-After header indicates backoff window

POST/api/public/share/{token}/unlock

const res = await fetch("https://filemv.com/api/public/share/Qm4t7Xz2Lp9Kd3Wf8Rb1Nc/unlock", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "password": "correct-horse-42"
  }),
});
const data = await res.json();
Response200application/json
{
  "share": {
    "adsEnabled": true,
    "archiveDownloadUrl": "https://filemv.com/…",
    "canonicalSlug": "string",
    "containingFolderShareToken": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
    "expiresAt": "2026-10-04T09:30:00Z",
    "files": [
      {
        "contentType": "string",
        "displayUrl": "https://filemv.com/…",
        "downloadUrl": "https://filemv.com/…",
        "exif": {
          "gpsPresent": true,
          "groups": null,
          "summary": [
            "1/500 s",
            "2026. 08. 12.",
            "35 mm",
            "6000×4000",
            "ISO 200",
            "Sony α7 IV",
            "ƒ/2.8"
          ]
        },
        "fileName": "vacation.jpg",
        "id": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
        "inlineUrl": "https://filemv.com/…",
        "paste": {
          "charCount": null,
          "language": null,
          "lineCount": null,
          "rawUrl": null,
          "revision": null,
          "updatedAt": null
        },
        "sizeBytes": 1048576
      }
    ],
    "folderName": "vacation.jpg",
  …
Try it

This endpoint changes data — run it from your own environment with the sample above.

API keys

GET/api/me/api-keysAPI key or session

List API keys

Lists the caller's API keys, newest first, revoked ones included. Rows carry the display prefix and lifecycle metadata, never the raw key. lastUsedAt is updated at most once a minute.

Responses

  • 200The caller's keys
  • 401Not authenticated (`authentication_required`)

GET/api/me/api-keys

const res = await fetch("https://filemv.com/api/me/api-keys", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
const data = await res.json();
Response200*/*

No response body.

POST/api/me/api-keysAPI key or session

Create an API key

Mints a new personal API key. The raw key is returned in rawKey exactly once: store it immediately, it can never be retrieved again (only a keyed hash is kept). The body and its label are optional.

Request bodyapplication/json

  • labelstring Optional free-text label (≤ 80 chars) to tell keys apart. Blank means none.

Responses

  • 201Key created — `rawKey` is shown only in this response
  • 400`label` longer than 80 characters (`validation_failed`)
  • 401Not authenticated (`authentication_required`)
  • 409The user already has 10 active keys (`api_key_limit_reached`)

POST/api/me/api-keys

const res = await fetch("https://filemv.com/api/me/api-keys", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "label": "CI server"
  }),
});
const data = await res.json();
Response201*/*

No response body.

Try it

This endpoint changes data — run it from your own environment with the sample above.

DELETE/api/me/api-keys/{id}API key or session

Revoke an API key

Revokes one of the caller's keys. Requests carrying it are rejected from then on. Revoking an already revoked key is a no-op.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

  • 204Revoked
  • 401Not authenticated (`authentication_required`)
  • 404No such key for the caller (`api_key_not_found`)

DELETE/api/me/api-keys/{id}

const res = await fetch("https://filemv.com/api/me/api-keys/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b", {
  method: "DELETE",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
console.log(res.status); // 204
Response204

No response body.

Try it

This endpoint changes data — run it from your own environment with the sample above.

Webhooks

GET/api/users/me/webhooksAPI key or session

List all registered webhooks (secret masked)

Returns all enabled and disabled webhook endpoints for the calling user. The signingSecretMasked field exposes only the last 4 characters of the 64-char secret.

Responses

  • 200Webhook list (may be empty)
  • 401Missing or invalid authentication

GET/api/users/me/webhooks

const res = await fetch("https://filemv.com/api/users/me/webhooks", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
const data = await res.json();
Response200application/json
[
  {
    "consecutiveFailures": 1,
    "createdAt": "2026-10-04T09:30:00Z",
    "disabledReason": "string",
    "enabled": true,
    "id": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
    "signingSecretMasked": "string",
    "url": "https://filemv.com/…"
  }
]
POST/api/users/me/webhooksAPI key or session

Register a new webhook endpoint

Creates a new webhook endpoint and returns the full HMAC signing secret once. Available on every plan. The URL must pass SSRF validation (400 webhook_url_blocked if not). The per-user limit is enforced (409 webhook_limit_reached if exceeded).

Request bodyapplication/json

  • urlstringrequired HTTPS delivery endpoint URL. Must pass SSRF validation.

Responses

  • 201Webhook registered — response body contains the one-time full signing secret
  • 400Validation failed (blank URL, URL too long, or URL blocked by SSRF filter)
  • 401Missing or invalid authentication
  • 409Per-user webhook limit reached (webhook_limit_reached)
  • 429Too many webhook mutations from this user — Retry-After header indicates the backoff window

POST/api/users/me/webhooks

const res = await fetch("https://filemv.com/api/users/me/webhooks", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "url": "https://example.com/webhook"
  }),
});
const data = await res.json();
Response201application/json
{
  "createdAt": "2026-10-04T09:30:00Z",
  "enabled": true,
  "id": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
  "signingSecret": "4f9c2b7e1a6d3c8f0b5e2a9d7c4f1e8b3a6d9c2f5e8b1a4d7c0f3e6b9a2d5c8f",
  "url": "https://example.com/webhook"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

PATCH/api/users/me/webhooks/{id}API key or session

Partially update a webhook (url and/or enabled state)

Both fields are optional — omitting a field is a no-op for that attribute. A URL change re-runs SSRF validation. Returns 404 if the webhook does not exist or belongs to another user.

Parameters

NameInTypeDescription
id requiredpathstring

Request bodyapplication/json

  • enabledboolean | null New enabled state. True re-enables. False disables. Null leaves the state unchanged.
  • urlstring | null New delivery endpoint URL. Must pass SSRF validation. Null leaves the URL unchanged.

Responses

  • 200Updated webhook (secret masked)
  • 400Validation failed (URL too long or URL blocked by SSRF filter)
  • 401Missing or invalid authentication
  • 404Webhook not found or belongs to another user

PATCH/api/users/me/webhooks/{id}

const res = await fetch("https://filemv.com/api/users/me/webhooks/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "url": "https://example.com/webhook"
  }),
});
const data = await res.json();
Response200application/json
{
  "consecutiveFailures": 1,
  "createdAt": "2026-10-04T09:30:00Z",
  "disabledReason": "string",
  "enabled": true,
  "id": "3f9c1e2a-7b4d-4c1e-9a2f-0d8e6b1c5a77",
  "signingSecretMasked": "string",
  "url": "https://filemv.com/…"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

DELETE/api/users/me/webhooks/{id}API key or session

Delete a webhook and its delivery history

Purges all webhook_delivery rows first, then removes the webhook row. Returns 404 if the webhook does not exist or belongs to another user.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

  • 204Webhook deleted
  • 401Missing or invalid authentication
  • 404Webhook not found or belongs to another user

DELETE/api/users/me/webhooks/{id}

const res = await fetch("https://filemv.com/api/users/me/webhooks/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b", {
  method: "DELETE",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
console.log(res.status); // 204
Response204

No response body.

Try it

This endpoint changes data — run it from your own environment with the sample above.

POST/api/users/me/webhooks/{id}/rotate-secretAPI key or session

Rotate the HMAC signing secret for a webhook

Generates a new 64-character HMAC signing secret. The old secret is immediately invalidated — any in-flight delivery signed with the old secret will fail verification. The full new secret is returned once; store it securely. Returns 404 if the webhook does not exist or belongs to another user.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

  • 200New signing secret (full, one-time)
  • 401Missing or invalid authentication
  • 404Webhook not found or belongs to another user
  • 429Too many webhook mutations from this user — Retry-After header indicates the backoff window

POST/api/users/me/webhooks/{id}/rotate-secret

const res = await fetch("https://filemv.com/api/users/me/webhooks/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b/rotate-secret", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
const data = await res.json();
Response200application/json
{
  "createdAt": "2026-10-04T09:30:00Z",
  "enabled": true,
  "id": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
  "signingSecret": "4f9c2b7e1a6d3c8f0b5e2a9d7c4f1e8b3a6d9c2f5e8b1a4d7c0f3e6b9a2d5c8f",
  "url": "https://example.com/webhook"
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

POST/api/users/me/webhooks/{id}/testAPI key or session

Send a one-shot test webhook delivery

Synchronous test call — no outbox row is persisted and no retry is scheduled. Returns the HTTP outcome immediately. Returns 404 if the webhook does not exist or belongs to another user.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

  • 200Test delivery outcome (check delivered field)
  • 401Missing or invalid authentication
  • 404Webhook not found or belongs to another user
  • 429Too many webhook mutations from this user — Retry-After header indicates the backoff window

POST/api/users/me/webhooks/{id}/test

const res = await fetch("https://filemv.com/api/users/me/webhooks/6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b/test", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.FILEMV_API_KEY}`,
  },
});
const data = await res.json();
Response200application/json
{
  "delivered": true,
  "error": "string",
  "statusCode": 1
}
Try it

This endpoint changes data — run it from your own environment with the sample above.

Webhooks — delivery and signatures

Register an HTTPS endpoint with POST /api/users/me/webhooks. Events arrive as signed JSON POST requests. Webhooks are available on every plan. Each user can have at most 5 webhooks. A URL pointing to a private, loopback or internal address is rejected with 400 webhook_url_blocked.

Verifying the signature

Every delivery carries an X-FileMV-Signature: t=<unix>,v1=<hex> header. v1 = HMAC-SHA256(key, "<t>." + raw body), in lowercase hex. The key is the 64-character signingSecret you got at registration, used as text (its UTF-8 bytes). Do not hex-decode it. Compute over the raw body, before any JSON parsing. Reject a request whose t is more than 5 minutes old. The server sends a fresh t on every delivery attempt.

import crypto from "node:crypto";

// rawBody: the request body exactly as received (Buffer), before JSON.parse
export function verifyFileMvSignature(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;

  const expected = crypto
    .createHmac("sha256", Buffer.from(secret, "utf8"))
    .update(`${t}.`)
    .update(rawBody)
    .digest("hex");
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(parts.v1 ?? "", "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Events

  • share.download — a recipient notified by email downloaded
  • share.download.anonymous — an anonymous download from a share
  • collect.uploaded — someone uploaded to your collect link
  • proofing.submitted — the client submitted their selection
  • webhook.test — the test delivery

Delivery

Only a 2xx response counts as success; redirects are not followed. Timeouts: 5 s to connect, 10 s to respond. On failure there are at most 6 attempts: immediately, then after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. If 10 deliveries in a row fail on every attempt, the webhook is disabled (disabledReason: auto_disabled_failures) and you get an email. Deduplicate on the X-FileMV-Delivery header.

Sample body

{
  "id": "evt_5b0f2c1e-8d7a-4e3b-9c6f-1a2b3c4d5e6f",
  "type": "share.download",
  "created": 1791105000,
  "data": {
    "share_token": "Qm4t7Xz2Lp9Kd3Wf8Rb1Nc",
    "share_name": "Holiday 2025",
    "recipient_email_domain": "example.com",
    "occurred_at": "2026-10-04T09:30:00Z"
  }
}

Changelog

  • Passwords: at most 72 bytes

    Share passwords (POST /api/files/{fileId}/share, POST /api/folders/{folderId}/share, PATCH …/share/password, POST /api/files/share-multi) and paste passwords (POST /api/pastes) are now limited to 72 bytes of UTF-8, down from 128 characters. That is 36 accented letters or 18 emoji. A longer password gets 400 password_too_long; some of these used to return a 500.

  • Webhooks on every plan

    The /api/users/me/webhooks endpoints (create, enable, test delivery, secret rotation) now work on the free plan too, and delivery no longer depends on the plan: the 403 webhook_paid_required response is gone.

  • Custom links on every plan

    PATCH /api/files/{fileId}/share/slug, PATCH /api/folders/{folderId}/share/slug and GET /api/share/slug-available now work on the free plan too: the 403 vanity_slug_paid_required response is gone. On these endpoints a 403 now only means the caller does not own the file or folder.

  • publicListing on the single-file anonymous upload too

    POST /api/public/uploads now accepts the optional publicListing field too, with the same meaning as on the batch endpoint: when true the share is PUBLIC (listed) and may be indexed by search engines; absent or false keeps it SHARED.

  • Listed shares at upload time: publicListing

    New optional publicListing field on POST /api/public/uploads/batch and (together with autoShare) on POST /api/uploads. When true the share is PUBLIC (listed) and may be indexed by search engines; absent or false keeps the previous behaviour (SHARED). The GET /api/public/share/{token} response has a new indexable field telling whether the share page may be indexed.

  • Public API reference published

    Documentation for the upload, file, sharing, API-key and webhook endpoints, with JavaScript, PHP and C# samples. Every /api response now carries X-RateLimit-Limit, -Remaining and -Reset headers.