Files API
File listing needs both the files:read scope and files.read on the token owner. Uploads and every chunk step need files:upload plus files.upload. Current role grants are rechecked; a stored token does not retain revoked authority. Without files.share, new uploads are private. An upload with expiration also needs files.delete for DELETE, or files.share for SET_PRIVATE, including expiration inherited from profiles/defaults; otherwise finalization returns 403. See scope and role intersection.
Use files:upload to add files and files:read to browse your account's metadata. The API uses the same storage and sharing policies as Flare's dashboard.
Archive uploads use the same upload contract and remain single stored files. Archive APIs provide owner-session browsing/extraction/creation and separate share-page browsing/entry downloads. Share reads use the file's visibility/password rules, while extraction and creation remain owner-only. Neither files:read nor files:upload authorizes these routes; existing named-token operations and response shapes are unchanged.
Upload one file
POST /api/files · Scope: files:upload · Body: multipart/form-data
Send one file in the file field. Use a separate request for each file. This route streams file bytes to storage; it does not require the entire file to fit in application memory.
curl --fail-with-body \
-H "Authorization: Bearer $FLARE_TOKEN" \
-F 'file=@./report.pdf;type=application/pdf' \
-F 'visibility=PRIVATE' \
-F 'expiration=WEEK' \
-F 'expiryAction=DELETE' \
"$FLARE_URL/api/files"Successful uploads return 200, with this shape:
{
"success": true,
"data": {
"url": "https://files.example.com/alex/report.pdf",
"pageUrl": "https://files.example.com/alex/report.pdf",
"rawUrl": "https://files.example.com/api/files/abc123/report.pdf",
"downloadUrl": "https://files.example.com/api/files/cm_example/download",
"copyText": "https://files.example.com/alex/report.pdf",
"name": "report.pdf",
"size": 204800,
"type": "application/pdf"
}
}| Response field | Meaning |
|---|---|
pageUrl | The Flare share page. Uses a vanity ID when the account has one. |
url, copyText | Compatibility aliases for pageUrl. |
rawUrl | Flare's raw-content route, using the underlying account URL ID. |
downloadUrl | Flare's download route. May redirect to storage after checking access. |
name | File's display name. |
size | File size in bytes. |
type | MIME type associated with the stored file. |
Returned Flare links obey the file's sharing settings. A link to a private file is not a public access grant, and supplying the upload token to a download URL does not unlock it. Private content requires an eligible owner browser session with files.read, or a browser session with content.read. A public password-protected file requires its password for other viewers.
After an allowed download, S3 can issue a temporary signed storage URL. That URL carries its own access grant and can remain usable until its expiry even if the Flare file's visibility or password changes. Changing Flare's access settings does not recall an already issued storage URL or a downloaded copy.
Flare chooses safe URL names and handles collisions. Always use the returned URL instead of predicting a path from the original filename. The public origin in these responses comes from the operator's NEXTAUTH_URL setting.
The URL name is built from the original filename, which is kept unchanged as the display name:
- The part after the last dot is the extension only when it is 1 to 16 ASCII letters or digits. It is lowercased. Otherwise the dot and that part stay in the name, so
notes 2026.10.25 finalhas no extension. - The rest is lowercased, and each run of characters other than ASCII letters and digits becomes one hyphen, with none at either end.
My Report.PDFbecomesmy-report.pdf, andnotes.h#mlbecomesnotes-h-ml. - A name with no ASCII letters or digits, such as
日本語.png, gets six random URL-safe characters before its extension, for examplex7Kp2a.png. Randomized naming uses the same random part and extension rule for every file. - If the URL is already used in your account, the upload is published with a hyphen and 12 characters of a random ID before the extension, such as
my-report-3f9c2a71-b04.pdf.
Uploads made before this rule kept their URLs. Earlier builds could put spaces, capitals, #, or a leading dot in new URL names, for example My Report-1.PDF or .png; those links keep working unchanged.
Signed-out visitors can open a public file's share page, /raw, and /direct whether or not its URL name has an extension. Earlier builds sent them to the sign-in page instead when the name had no extension, such as /abc12/readme, or started with a dot, such as /abc12/.png; those links now open too. Private files and passwords are checked by the routes as before. A first path segment that belongs to Flare itself (api, auth, dashboard, admin, settings, setup, raw, direct, u, profile, avatars, or _next) is never read as a URL ID, which is also why vanity IDs can't use those words.
Select a profile and folder before sending bytes
Use request headers or query parameters for decisions that affect where and how the upload starts:
| Selection | Header | Query alternative | Special value |
|---|---|---|---|
| Upload profile | X-Upload-Profile: PROFILE_ID | ?profileId=PROFILE_ID | none bypasses your account's default profile. |
| Destination folder | X-Upload-Folder: FOLDER_ID | ?folderId=FOLDER_ID | none leaves the file unfiled. |
IDs must belong to your account. If both header and query are supplied, they must agree. A bound token cannot use a different profile or none.
curl --fail-with-body \
-H "Authorization: Bearer $FLARE_TOKEN" \
-H "X-Upload-Profile: $FLARE_PROFILE_ID" \
-H "X-Upload-Folder: $FLARE_FOLDER_ID" \
-F 'file=@./screenshot.png' \
"$FLARE_URL/api/files"For ordinary multipart uploads, a profileId or folderId form field cannot change a selection after the upload starts. Choose them in the header/query instead. Likewise, choose URL randomization in a saved profile or your account defaults before sending the file; a differing multipart randomizeFileUrls value is rejected.
Upload options
Omitting an option inherits the selected profile or account/default value. Supplying an explicit value overrides it, subject to bound-token restrictions.
| Option | Values | Behavior |
|---|---|---|
visibility | PUBLIC, PRIVATE | Public files are shareable; private files require an eligible owner/admin session. Default without a profile override is PUBLIC. |
password | String, or null | Adds a password for other viewers: at least 8 characters and at most 72 UTF-8 bytes; otherwise 400. Omitted means no password unless already selected for this chunk session; empty/null clears it. |
expiration | DISABLED, HOUR, DAY, WEEK, MONTH | Relative expiration. DISABLED explicitly turns expiration off. Month means a UTC calendar-month increment, not a fixed 30 days. |
expiresAt | Future ISO 8601 timestamp with timezone, or null | Absolute expiration override. null explicitly disables expiration. A supplied value takes precedence over the relative duration. The timezone is Z or an offset from -23:59 to +23:59, such as +02:00 or +0200; seconds are optional. |
expiryAction | DELETE, SET_PRIVATE | Delete the file or make it private when its scheduled expiration is processed. |
shareStyle | minimal, framed, delivery | The presentation style saved for this upload's share page. |
tagIds | Array of owned tag IDs, maximum 20 | Adds the selected profile/manual tags. For multipart forms, send a JSON array encoded as a string. |
randomizeFileUrls | Boolean | Randomized URL naming. Set before upload via profile/account settings, or in the chunk initialization JSON. |
profileId | Owned profile ID, or null | Selects a profile. For multipart use the header/query described above; chunk initialization also accepts JSON. |
folderId | Owned folder ID, or null | Selects a destination folder. This is a request option, not a saved profile option. |
Multipart fields are strings: use true/false for booleans, and an empty string or null string for nullable password, expiresAt, profileId, and folderId. In JSON bodies use actual booleans, arrays, and null.
Earlier versions accepted file passwords of any length up to 72 bytes. Clients that send a password shorter than 8 characters now receive 400 with {"error":"File passwords must be at least 8 characters."} and no file is created, so update scripts and screenshot-tool configurations that set one. PATCH /api/files/{id} applies the same rule when protecting an existing file. Passwords already stored on files keep working.
curl --fail-with-body \
-H "Authorization: Bearer $FLARE_TOKEN" \
-F 'file=@./diagram.png' \
-F 'tagIds=["TAG_ID"]' \
-F 'shareStyle=minimal' \
-F 'expiration=DISABLED' \
"$FLARE_URL/api/files"Base defaults are public visibility, no expiration, delete on expiration, original-name URLs, no tags, and framed sharing. Account defaults supply naming and expiration settings, and the instance's published sharing configuration supplies the base share style. A selected profile overrides those defaults; permitted request options take final precedence. The retired copyFormat option is ignored for compatibility.
Upload errors and limits
| Status | Common cause | Recovery |
|---|---|---|
400 | Missing file; wrong content type; invalid options; changing profile/folder/naming too late; duplicate chunk part; expiration already passed; file type mismatch | Fix the request. Select profile and folder before upload. |
401 | Authentication or scope failure | Check the authentication guide. |
403 | Profile binding conflict; token invalidated before finalization | Use the allowed profile or replace the credential. |
404 | Selected profile/folder/tag or chunk session not found | Verify ownership and IDs. Start a new chunk session if it expired. |
409 | Storage configuration changed during chunk upload; part sent after completion | Start again after a storage change. Do not upload new parts to a completed session. |
413 | Instance upload limit, remaining quota, or part-size limit exceeded | Check the instance limit, free storage, or send smaller parts. |
429 | Too many upload starts | Honor Retry-After: 60. |
500 | Upload/storage operation failed | Check instance health and operator logs. Avoid blindly repeating a multipart upload if its response was lost. |
POST /api/files and POST /api/files/chunks share a limit of 30 requests per 60 seconds per client IP per application process. It runs before authentication. Limits reset on process restart and are not a distributed rate-limit service. Proxies should supply accurate client IP headers. Other file-read and part/completion routes do not use this upload-start limiter.
Direct uploads and chunk parts can use the full limits described here. Earlier builds truncated any API request body over 10 MB, so direct uploads above that failed with 500, and uploads with larger parts failed at completion with 400; see aligning upload limits. Maximum file size and quota are operator settings, not fixed API constants. Flare uses powers of 1024 for its MB/GB limits. Accounts with quotas.bypass or Administrator are exempt from the default quota, but still subject to the maximum upload size. Quota and size are checked again when a file is finalized, so concurrent uploads cannot rely only on the earlier quota check.
The server checks detected file bytes against the claimed MIME type. A successful file-ready event means the upload was committed; optional OCR may finish later. Ordinary multipart uploads have no idempotency-key parameter: retrying after a lost response can create another file.
Multipart filenames are read as UTF-8, which is what browsers, curl, and most HTTP libraries send, so café 日本語.png is stored as written. A client can instead declare the encoding with an RFC 5987 filename* parameter, such as filename*=UTF-8''caf%C3%A9.png. Builds before this change decoded a plain filename as Latin-1, so names with non-ASCII characters were stored garbled; those files keep their stored names.
If Flare rejects an upload after reading the filename but before storing the bytes, for example because of its name or upload options, it answers at once with the error and discards the rest of the request body. Earlier builds could leave such a request waiting with no response until the client timed out.
A multipart upload that exceeds the maximum file size or the remaining quota always returns 413. Flare stops reading the body as soon as either limit is reached and keeps none of the file, even when the limit is reached before Flare has started writing to storage. Earlier builds could miss the limit when it was reached before Flare had prepared the upload's destination, which is likely when little quota remains and the request arrives quickly. They then stored the first part of the file and returned success: with 1 KB of quota left, a 64 KB upload was saved as a 1,024-byte file. Check recent uploads from accounts close to their quota if the file sizes look wrong.
List and search files
GET /api/files · Scope: files:read
Returns only files owned by the authenticated account, including its private files. Results use deterministic ordering and include pagination metadata.
curl --fail-with-body --get \
-H "Authorization: Bearer $FLARE_TOKEN" \
--data-urlencode 'search=invoice' \
--data-urlencode 'types=application/pdf,image/png' \
--data-urlencode 'sortBy=newest' \
--data-urlencode 'limit=24' \
"$FLARE_URL/api/files"| Parameter | Default | Meaning |
|---|---|---|
page | 1 | Positive integer, starting at 1. |
limit | 24 | Positive integer, capped at 100. |
ids | All files | One comma-separated list of 1–100 distinct file IDs, each 1–128 characters without whitespace or ASCII control characters. Combined with ownership and every other supplied filter. |
search | Empty | Case-insensitive match against filename or stored OCR text. |
sortBy | newest | newest, oldest, largest, smallest, name, most-viewed, least-viewed, most-downloaded, least-downloaded. Unrecognized values use newest order. |
types | All types | Comma-separated exact MIME types, such as image/png,image/jpeg. |
dateFrom | No lower bound | Inclusive upload date/time lower bound. Prefer ISO 8601 timestamps with a timezone. |
dateTo | No upper bound | Inclusive upper bound. A date-only value includes the final day using server-local time; explicit timestamps preserve the supplied instant. |
visibility | All files | Comma-separated public, private, hasPassword. Multiple values are ORed together. |
folder | All folders | Owned folder ID or unfiled. A folder filter matches that folder directly, not its descendants. |
tag | All tags | Owned tag ID or untagged. Excluded automatic tags do not count as active tags. |
snapshot | No ceiling | Inclusive upload timestamp ceiling returned by the timeline endpoint; send it unchanged on each window request. |
galleryAnchor | None | An image's file ID, used together with galleryDirection for neighboring images. |
galleryDirection | None | next or previous; requires galleryAnchor. |
Different filter categories combine with AND. For example, types=image/png&visibility=private,hasPassword returns PNGs that are private or password-protected.
{
"success": true,
"data": [
{
"id": "cm_example",
"name": "invoice.png",
"urlPath": "/abc123/invoice.png",
"mimeType": "image/png",
"size": 0.1953125,
"uploadedAt": "2026-09-20T12:00:00.000Z",
"visibility": "PRIVATE",
"views": 0,
"downloads": 0,
"folderId": null,
"user": { "urlId": "abc123" },
"tags": [{ "id": "TAG_ID", "name": "Finance" }],
"hasPassword": false,
"expiresAt": null
}
],
"pagination": { "total": 1, "pageCount": 1, "page": 1, "limit": 24 }
}Size units differ by endpoint
The file list's size is in MiB (bytes / 1,048,576). Upload responses and webhook sizeBytes are in bytes. Convert explicitly when comparing them.
To refresh metadata for a retained selection, send only ids and limit=100, without search, date, folder, tag, or snapshot filters. Results still use the requested sort order, not ID-input order. Match rows by id. Deleted files and IDs belonging to another account are omitted without revealing which case applies; compare the returned IDs with your selection before offering a bulk operation. This remains a metadata read: it does not grant file-content access or lock the files against later changes. Existing list requests without ids behave as before.
Set FLARE_FILE_IDS to comma-separated IDs from your own file-list response.
curl --fail-with-body --get \
-H "Authorization: Bearer $FLARE_TOKEN" \
--data-urlencode "ids=$FLARE_FILE_IDS" \
--data-urlencode 'limit=100' \
"$FLARE_URL/api/files"Empty IDs, duplicate IDs, whitespace or ASCII control characters, overlong IDs, more than 100 IDs, or repeated ids parameters return 400. The regular page and limit rules still apply; omitting limit returns at most 24 records.
The response excludes password hashes and OCR text. hasPassword indicates protection; search can match OCR without returning the text itself. expiresAt is a timestamp or null. An empty library has data: [], total: 0, and pageCount: 0. List responses include Cache-Control: private, no-store.
Normal pages use offsets, so a library changing between requests can move entries between pages. The optional snapshot ceiling excludes files uploaded after that instant; it does not freeze deletions, folder/tag edits, sharing changes, or mutable sort values. Invalid dates, snapshots, or visibility values return 400. For image navigation, provide both gallery parameters. Anchored requests restrict results to images within your other filters, exclude the anchor itself, and return neighbors in the requested direction. Their pagination also includes an offset. A missing or filtered-out anchor returns 404; supplying only one gallery parameter returns 400.
Check raw view counts
views includes share-page visits and successful GET /{userUrlId}/{filename}/raw responses. Each full response or 206 range response increments the total once. Raw access denials, missing files, failures opening storage, and HEAD requests do not increment it. This counts requests, including bots and repeated media ranges, rather than unique visitors or completed transfers. Existing totals are preserved after upgrading; earlier raw traffic is not backfilled. See sharing and privacy for caching and direct-storage limits.
The stored file must open successfully before Flare writes the counter. A local file-open failure or failed counter write returns 500 and closes the acquired stream. After resolving the storage or database failure, retry the request.
To check a disposable public file without a password that you own, set FLARE_FILE_IDS to its ID and FLARE_FILE_PATH to its URL-encoded urlPath from the list response, such as /abc123/fixture.txt. The file must contain at least three bytes. Run the metadata read below before and after the raw requests; in an otherwise idle fixture, views increases by two. The bearer token needs files:read for the metadata read. Raw content uses the file's public access and receives no bearer token.
curl --fail-with-body --get \
-H "Authorization: Bearer $FLARE_TOKEN" \
--data-urlencode "ids=$FLARE_FILE_IDS" \
"$FLARE_URL/api/files"
curl --fail-with-body --output /dev/null "$FLARE_URL$FLARE_FILE_PATH/raw"
curl --fail-with-body --range 0-2 --output /dev/null "$FLARE_URL$FLARE_FILE_PATH/raw"
curl --fail-with-body --head "$FLARE_URL$FLARE_FILE_PATH/raw"
curl --fail-with-body --get \
-H "Authorization: Bearer $FLARE_TOKEN" \
--data-urlencode "ids=$FLARE_FILE_IDS" \
"$FLARE_URL/api/files"Content caching
File content responses set Cache-Control from the file's access state, so a shared cache never stores bytes that needed a session or a password:
| Response | Private or password-protected | Public, no password |
|---|---|---|
GET /api/files/{path} (the rawUrl in upload responses), 200/206 | private, no-store | public, max-age=31536000 for video/* content, otherwise no-cache |
GET /{userUrlId}/{filename}/raw, 200/206 | private, no-store | No Cache-Control header |
GET/POST /api/files/{id}/download, including a signed S3 redirect | private, no-store | No Cache-Control header |
GET /api/files/{id}/thumbnail | private, no-store | public, max-age=31536000, immutable |
Access denials from these routes, 404 for a private file and 401 for a missing or wrong password, also send private, no-store. Archive listings and entries always do. There is no Vary header: a public, unprotected response is the same for every requester, and the others are never stored. A response cached while a file was public stays cached after it becomes private or gets a password; see browser and CDN caching.
Browse the whole file timeline
GET /api/files/timeline · Scope: files:read · Account permission: files.read
This lightweight endpoint supplies calendar bucket counts and offsets for a virtual library or date scrollbar. It applies the same ownership, selected-ID, search, type, date, visibility, folder, and tag filters as GET /api/files, using a database-clock read and one aggregate rather than returning every file's metadata. Counts include private files owned by the authenticated account, and never another account's files.
curl --fail-with-body --get \
-H "Authorization: Bearer $FLARE_TOKEN" \
--data-urlencode 'sortBy=newest' \
--data-urlencode 'groupBy=month' \
--data-urlencode 'timezone=America/Los_Angeles' \
"$FLARE_URL/api/files/timeline"| Parameter | Default | Meaning |
|---|---|---|
ids, search, types, dateFrom, dateTo, visibility, folder, tag | Same as the file list | All filter rules above apply. |
sortBy | newest | Date order produces calendar buckets. Other supported sorts produce one undated bucket. Unknown values use newest. |
groupBy | none | none, month, week, or year. none uses month buckets internally, without requesting visible headings. Weeks start Monday. |
timezone | UTC | IANA time zone for calendar boundaries, such as America/Los_Angeles. Browser clients can use their local IANA zone. |
{
"success": true,
"data": {
"total": 124,
"snapshot": "2026-10-06T12:00:00.000Z",
"groupBy": "month",
"timezone": "America/Los_Angeles",
"buckets": [
{
"key": "2026-10-01T07:00:00.000Z",
"from": "2026-10-01T07:00:00.000Z",
"to": "2026-11-01T07:00:00.000Z",
"count": 24,
"offset": 0
},
{
"key": "2026-09-01T07:00:00.000Z",
"from": "2026-09-01T07:00:00.000Z",
"to": "2026-10-01T07:00:00.000Z",
"count": 100,
"offset": 24
}
]
}
}from is inclusive and to exclusive. Both are ISO UTC instants for local calendar boundaries; daylight-saving changes can make a week or month shorter or longer. offset is the number of matching files before this bucket in the selected order. Empty periods are omitted. newest returns newest buckets first; oldest returns oldest first. key equals from. Non-date sorts return groupBy: "none" and one bucket with key: "all", null boundaries, and the total count. An empty result returns total: 0 and buckets: []. groupBy otherwise preserves the requested value, including none.
Treat both boundaries as string | null when generating a client from the OpenAPI 3.1 contract. For example, a nonempty sortBy=largest response contains an undated bucket like this; do not parse its null boundaries as dates:
{ "key": "all", "from": null, "to": null, "count": 124, "offset": 0 }To load a visible window, request GET /api/files with the same filters and sort, the returned snapshot, and a bounded limit (maximum 100). Within a dated bucket, set dateFrom to the later of the original lower bound and bucket.from; set dateTo to the earlier of the original upper bound and one millisecond before bucket.to. Page numbers start at 1 inside that filtered bucket, not at the bucket's global offset. With the undated bucket, keep the original date filters. Never replace a user's narrower date range with the whole bucket.
This executable Node.js example loads the first window of the oldest populated month. It uses a named token from the environment and does not put credentials in a URL:
const origin = process.env.FLARE_URL.replace(/\/$/, '')
const headers = { Authorization: `Bearer ${process.env.FLARE_TOKEN}` }
const filters = new URLSearchParams({ sortBy: 'oldest' })
const timelineQuery = new URLSearchParams(filters)
timelineQuery.set('timezone', 'UTC')
const summaryResponse = await fetch(
`${origin}/api/files/timeline?${timelineQuery}`,
{ headers }
)
if (!summaryResponse.ok) throw new Error(await summaryResponse.text())
const { data: timeline } = await summaryResponse.json()
const bucket = timeline.buckets[0]
if (bucket) {
const windowQuery = new URLSearchParams(filters)
windowQuery.set('snapshot', timeline.snapshot)
windowQuery.set('limit', '48')
windowQuery.set('page', '1')
if (bucket.from) windowQuery.set('dateFrom', bucket.from)
if (bucket.to)
windowQuery.set('dateTo', new Date(Date.parse(bucket.to) - 1).toISOString())
const windowResponse = await fetch(`${origin}/api/files?${windowQuery}`, {
headers,
})
if (!windowResponse.ok) throw new Error(await windowResponse.text())
console.log(await windowResponse.json())
}The timeline creates a new snapshot ceiling from PostgreSQL’s clock on every request, matching the clock used for upload timestamps even when the application host’s clock differs. It is an upload-time boundary, not a durable database snapshot or continuation token. Files uploaded later are excluded from subsequent windows that retain that ceiling. Deletions, backdated inserts, sharing/folder/tag changes, and changes to size/name/view/download sorts can still move results; fetch a fresh timeline and clear cached windows after a mutation or when refreshing. Offset reads within a very large bucket or a non-date sort can still cost more than early pages. Ordinary page clients and anchored image navigation remain supported unchanged.
Timeline responses use Cache-Control: private, no-store. Invalid groupings, time zones, dates, visibility, or selected-ID filters return 400; auth and current-role failures use the same 401/403 behavior as listing. Pagination and gallery parameters do not apply to timeline summaries.
File-list and timeline reads can appear as request-level http.get events in the instance audit log. These include the acting account and applicable token ID when available, without recording the bearer secret. The log uses its existing access, retention, and best-effort rules; reading metadata does not itself create file-ready webhook events.
Byte ranges
GET /{userUrlId}/{filename}/raw, GET /api/files/{path}, and GET /api/files/{id}/download honor one Range header per request, so media players can seek and download tools can resume. Content-Length always matches the bytes in the body.
Range header | Response |
|---|---|
bytes=0-1023 | 206 with bytes 0 through 1,023 and Content-Range: bytes 0-1023/<size> |
bytes=1024- | 206 from byte 1,024 to the end |
bytes=-500 | 206 with the last 500 bytes, or the whole file if it is shorter |
A last byte past the end, such as bytes=0-999999999 | 206 that stops at the file's last byte; Content-Range names the bytes actually sent |
A first byte at or past the end, bytes=-0, or any range of an empty file | 416 with Content-Range: bytes */<size> and no body |
Several ranges such as bytes=0-99,200-299, a reversed range such as bytes=9-1, or garbage | 200 with the whole file; the header is ignored, which HTTP allows for ranges it can't use |
A 416 keeps the content routes' Content-Security-Policy: sandbox and X-Content-Type-Options: nosniff headers. It does not count as a raw view, and the audit log records it as a failed file.read or file.download request. Access checks run first, so a private or password-protected file returns 404 or 401 before its size is revealed.
On S3 storage, GET /api/files/{id}/download can redirect to a temporary signed URL before reading the header. S3 then handles the Range itself: a single range gets 206, and an unsatisfiable one gets 416. /raw and /api/files/{path} read through Flare on both storage providers.
Earlier builds parsed Range loosely. On local storage, a suffix range, a reversed or malformed range, or a first byte past the end returned 500 from /raw and 404 from /api/files/{path}. On S3, a suffix range returned bytes from the start of the file under Content-Range: bytes NaN-…. On both, a last byte past the end advertised a Content-Length larger than the file, so clients reported a truncated transfer.
To check a disposable public file without a password, reuse FLARE_URL and FLARE_FILE_PATH from Check raw view counts. The first command prints the file's last five bytes. The second prints the status and Content-Range for a range past the end (curl 7.84 or later for %header{}):
curl --fail-with-body --range -5 "$FLARE_URL$FLARE_FILE_PATH/raw"
curl --silent --output /dev/null \
--write-out '%{http_code} %header{content-range}\n' \
--range 999999999- "$FLARE_URL$FLARE_FILE_PATH/raw"Discover file types
GET /api/files/types · Scope: files:read
Returns the distinct MIME types currently present in your account, sorted alphabetically. This is not a list of server-allowed upload formats.
{
"success": true,
"data": { "types": ["application/pdf", "image/jpeg", "image/png"] }
}Read current tags for a selection
GET /api/files/tags?fileIds=FILE_ID,OTHER_FILE_ID · Account browser session or legacy account upload credential · Permission: files.read
This dashboard route supplies fresh tag membership when opening Edit tags, including selected files outside the visible library window. It does not accept named flr_… tokens and is excluded from the named-token request builder and OpenAPI paths. Existing integrations using the named-token file listing can continue reading each file's tags there.
Send one fileIds parameter containing 1–100 comma-separated, nonempty IDs, each at most 128 characters after trimming. Duplicate IDs are returned once in their first-requested order; the 100-entry limit applies before deduplication. Every file must still belong to the authenticated account. Only active tag assignments are returned, sorted by normalized tag name, then ID. The read does not modify files, tag rules, or manual exclusions, and emits no webhook event.
For a signed-in browser on your disposable local Flare instance, this console example reads two files already returned by its file listing:
const query = new URLSearchParams({ fileIds: [fileId, otherFileId].join(',') })
const response = await fetch(`/api/files/tags?${query}`, {
credentials: 'same-origin',
cache: 'no-store',
})
const result = await response.json()
if (!response.ok) throw new Error(result.error || 'Could not read file tags')
console.log(result.data.files)Set fileId and otherFileId to owned file IDs from that disposable account. Success is 200 with Cache-Control: private, no-store:
{
"success": true,
"data": {
"files": [
{ "id": "FILE_ID", "tags": [{ "id": "TAG_ID", "name": "Review" }] },
{ "id": "OTHER_FILE_ID", "tags": [] }
]
}
}Malformed or oversized selections return 400. Missing authentication or a named token returns 401; a missing current files.read grant returns 403. If any ID is missing, deleted, or belongs to another account, the whole request returns the same generic 404, without a partial result. Close the editor, refresh the library, and select the remaining files again. Unexpected backend failures return 500; retry the read. A successful response reflects this read; it does not lock out later edits by another client.
The audit log records a request-level http.get outcome with the verified account when authentication succeeds. It does not include the queried file IDs or returned tag names. An unavailable-file 404 is recorded as a failure; the response and audit summary do not distinguish a missing file from another account’s file.
Chunked uploads
Chunked uploads let a client transfer parts before finalizing one file. Both local and S3 storage support Flare's authenticated part-upload route. S3 can additionally provide direct presigned part URLs.
All steps require files:upload. Use the same account throughout and keep the returned uploadId and each part's ETag. Flare's dashboard uses 5 MiB parts; choose part sizes compatible with your storage provider. Parts sent through Flare must be at most 64 MiB and cannot exceed the declared total file size.
1. Start the upload
POST /api/files/chunks · Body: application/json
curl --fail-with-body \
-H "Authorization: Bearer $FLARE_TOKEN" \
-H 'Content-Type: application/json' \
--data '{"filename":"archive.zip","mimeType":"application/zip","size":10485760,"visibility":"PRIVATE"}' \
"$FLARE_URL/api/files/chunks"Required fields are filename (1–255 characters), mimeType (1–255 characters), and size (positive integer bytes). The body also accepts the upload options above, including profileId, folderId, and randomizeFileUrls. Header/query profile/folder choices must agree with JSON choices if both are present. Unknown JSON fields are rejected, except the retired copyFormat compatibility field.
filename names the file; it never selects a folder or storage location. Flare keeps only the part after the last / or \ as the display name and removes control characters, so photos/2026/shot.png and C:\Users\me\shot.png are both stored as shot.png. Multipart uploads to POST /api/files treat the form's filename the same way. A chunked filename with nothing left after that, such as folder/ or .., returns 400 with Invalid filename.; a multipart upload in the same situation is named file. Earlier versions stored a chunked filename exactly as sent, so clients that put a path there now get only its last part. Files uploaded before this change keep their saved names.
{
"data": {
"uploadId": "0123456789abcdef0123456789abcdef",
"fileKey": "uploads/abc123/unique-object/archive.zip"
}
}Treat fileKey as an opaque internal key. It is not a share link or proof that a file has been published.
2. Upload parts through Flare
PUT /api/files/chunks/{uploadId}/part/{partNumber} · Body: raw file bytes
curl --fail-with-body --request PUT \
-H "Authorization: Bearer $FLARE_TOKEN" \
-H 'Content-Type: application/octet-stream' \
--data-binary '@./part-1.bin' \
"$FLARE_URL/api/files/chunks/$UPLOAD_ID/part/1"{ "data": { "etag": "\"provider-etag\"" } }Number parts from 1 through 10,000. Save the exact returned data.etag, including any quote characters in the value. The response field is lowercase etag; completion expects uppercase ETag. Retry a part using the same part number if necessary, and retain the ETag from the successful final write.
Optional: send parts directly to S3
GET /api/files/chunks/{uploadId}/part/{partNumber} returns:
{ "data": { "url": "https://storage.example.com/presigned-part-url" } }For S3, PUT the raw part bytes to that URL and retain the storage response's ETag header. The URL already carries storage authorization; do not send your Flare bearer token to it. Presigned URLs expire after one hour. Browser clients also need appropriate storage CORS rules, including access to the ETag response header.
For local storage, the returned value uses the local:// scheme and cannot be fetched over HTTP. Upload via Flare's authenticated PUT route instead.
The compatibility endpoint GET /api/files/chunks?uploadId=…&partNumber=… performs the same lookup but returns both aliases: { "data": { "url": "…", "presignedUrl": "…" } }. It is a part-URL endpoint, not a status or list-parts endpoint.
3. Complete the upload
POST /api/files/chunks/{uploadId}/complete · Body: application/json
{
"parts": [
{ "PartNumber": 1, "ETag": "\"first-etag\"" },
{ "PartNumber": 2, "ETag": "\"second-etag\"" }
]
}Send every part once. There must be 1–10,000 entries with unique integer part numbers. ETags must be nonempty strings of at most 512 characters. Flare sorts parts by number, assembles the object, verifies its actual size equals your declared size, rechecks policy, and creates the file.
This endpoint returns the upload-link object directly:
{
"url": "https://files.example.com/abc123/archive.zip",
"pageUrl": "https://files.example.com/abc123/archive.zip",
"rawUrl": "https://files.example.com/api/files/abc123/archive.zip",
"downloadUrl": "https://files.example.com/api/files/cm_example/download",
"copyText": "https://files.example.com/abc123/archive.zip",
"name": "archive.zip",
"size": 10485760,
"type": "application/zip"
}Alternatively, PUT /api/files/chunks accepts the same body plus uploadId and returns { "data": { ...uploadLinks } }. Use one completion route consistently in your client.
Completion accepts permitted upload-option overrides, but cannot change the profile, folder, or naming strategy chosen at initialization. Bound-token restrictions still apply. Repeating completion after a lost response returns the existing file while the upload session remains available; it does not publish another copy or enqueue another normal file-ready event.
Session lifetime and recovery
Upload metadata expires after one hour of inactivity. Requesting a part URL or successfully sending a part updates activity. Completion does not create an indefinite retention guarantee. The session ID is scoped to its account and, for bound tokens, its profile.
Metadata is stored under tmp/uploads on the application filesystem. Multi-replica deployments must ensure all requests for a chunk session can reach its metadata and, for local storage, its temporary parts. An ephemeral restart can lose an in-progress upload even when finalized files are safely stored in S3.
Changing storage provider/configuration during an upload causes 409; start again against the current storage. Each new chunk session also records the actual provider identity that initialized its parts. Part-URL, part-write, and completion requests return 409 when that provider differs or the session lacks recorded provenance, including sessions started before the storage-provenance upgrade. Restart the entire affected upload with a new initialization request; repeatedly resending completion or old part numbers cannot repair it. Successfully completed files and successful request/response formats are unchanged. Flare currently has no API for cancelling a chunk session or querying which parts were uploaded. Preserve your own part state, let stale sessions expire, and configure storage cleanup for abandoned multipart objects as part of server operations.