API v1

File Management
as a Service

Upload files directly to S3 via presigned URLs, serve images through a CDN with on-demand resizing, and manage everything through a simple REST API.

01

Quick Start

Get files uploading in four steps. Create a project, request an upload URL, push the file directly to S3, then confirm.

1Create a Project

Sign in to the dashboard and create a new project. You'll receive an API key in the format xf_live_{nanoid(32)}. This key is shown once — copy it immediately.

2Request an Upload URL

POST /api/v1/files/upload-intent
curl -X POST https://xfiles.dev/api/v1/files/upload-intent \
-H "Authorization: Bearer xf_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"filename": "photo.jpg",
"contentType": "image/jpeg",
"size": 204800,
"ownerUserId": "user_123",
"entity": {
"type": "post",
"id": "post_abc123",
"role": "gallery"
}
}'
Response
{
"fileId": "abc123def456",
"uploadUrl": "https://xfiles-storage.s3.amazonaws.com/...",
"key": "files/originals/proj_id/abc123def456/photo-k9x2m7.jpg",
"expiresIn": 900
}

3Upload Directly to S3

PUT the file body to the presigned URL. The Content-Type and Content-Length must match exactly what you declared in step 2 — S3 will reject mismatches with 403.

PUT to presigned URL
curl -X PUT "UPLOAD_URL_FROM_STEP_2" \
-H "Content-Type: CONTENT_TYPE_FROM_STEP_2" \
--data-binary @photo.jpg

4Confirm the Upload

POST /api/v1/files/confirm
curl -X POST https://xfiles.dev/api/v1/files/confirm \
-H "Authorization: Bearer xf_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "fileId": "abc123def456" }'
Response
{
"file": {
"id": "abc123def456",
"filename": "photo-k9x2m7.jpg",
"originalFilename": "photo.jpg",
"contentType": "image/jpeg",
"size": 204800,
"visibility": "private",
"uploadStatus": "confirmed",
"ownerUserId": null,
"entity": {
"type": "post",
"id": "post_abc123",
"role": "gallery"
},
"createdAt": "2026-03-12T10:00:00.000Z"
}
}

How it works: Files never pass through your application server. The presigned URL lets the client upload directly to S3 — your server only handles the intent and confirmation steps.

02

Authentication

All /api/v1/* routes authenticate via Bearer token using project API keys.

Authorization header
Authorization: Bearer xf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Formatxf_live_{nanoid(32)} — 38 characters total
Created viaDashboard — New Project — key shown once, copy immediately
RotationDashboard — Project Settings — Rotate Key — old key immediately invalidated
ErrorsMissing or invalid key returns 401 Unauthorized
03

API Reference

File EndpointsAPI Key Auth

MethodPathDescription
POST/api/v1/files/upload-intentGet presigned upload URL
POST/api/v1/files/confirmConfirm upload after S3 PUT
GET/api/v1/filesList files with filters
GET/api/v1/files/{id}Get file details + signed URL
PATCH/api/v1/files/{id}Update visibility or owner
DELETE/api/v1/files/{id}Delete single file from S3 + DB
DELETE/api/v1/filesBulk delete by entity filters
POST/api/v1/files/{id}/urlGenerate fresh signed URL
POST/api/v1/files/upload-intent

Creates a pending file record and returns a presigned S3 PUT URL. The presigned URL locks Content-Type and Content-Length so the client cannot upload a different file than declared.

Request Body
{
"filename": "photo.jpg", // Required — original filename
"contentType": "image/jpeg", // Required — locked in presigned URL
"size": 204800, // Required — locked in presigned URL
"visibility": "public", // Optional — default "private"
"ownerUserId": "user_123", // Optional — track who uploaded
"entity": { // Optional — attach to an entity
"type": "post", // Polymorphic entity type
"id": "post_abc123", // Polymorphic entity ID
"role": "gallery" // Optional — avatar, logo, gallery, document, etc.
}
}
Response
{
"fileId": "abc123def456",
"uploadUrl": "https://xfiles-storage.s3.amazonaws.com/...",
"key": "files/originals/proj_id/abc123def456/photo-k9x2m7.jpg",
"expiresIn": 900
}
POST/api/v1/files/confirm

Verifies the file exists in S3, updates status to confirmed, increments storage usage, and auto-creates a file attachment if entity was provided at upload.

Request Body
{
"fileId": "abc123def456"
}
Response
{
"file": {
"id": "abc123def456",
"filename": "photo-k9x2m7.jpg",
"originalFilename": "photo.jpg",
"contentType": "image/jpeg",
"size": 204800,
"visibility": "public",
"uploadStatus": "confirmed",
"ownerUserId": "user_123",
"entity": {
"type": "post",
"id": "post_abc123",
"role": "gallery"
}
}
}
GET/api/v1/files

List confirmed files for the authenticated project. Supports filtering and pagination.

Example
curl -G https://xfiles.dev/api/v1/files \
-H "Authorization: Bearer xf_live_your_api_key" \
-d "entityType=post" \
-d "entityId=post_abc123" \
-d "role=gallery" \
-d "ownerUserId=user_123" \
-d "visibility=public" \
-d "page=1" \
-d "limit=20"
Response
{
"files": [ ... ],
"total": 42,
"page": 1,
"limit": 20,
"totalPages": 3
}
GET/api/v1/files/{id}

Returns full file details, a signed original URL, and CDN variant URLs for images. Raster images include resized variants at predefined widths. SVGs include a single passthrough URL.

Example
curl https://xfiles.dev/api/v1/files/abc123def456 \
-H "Authorization: Bearer xf_live_your_api_key"
Response
{
"file": {
"id": "abc123def456",
"filename": "photo-k9x2m7.jpg",
"originalFilename": "photo.jpg",
"contentType": "image/jpeg",
"size": 204800,
"visibility": "public",
"uploadStatus": "confirmed",
"ownerUserId": "user_123",
"entity": {
"type": "post",
"id": "post_abc123",
"role": "gallery"
}
},
"url": "https://d1zdv6x2we68mp.cloudfront.net/files/originals/...?Signature=...",
"variants": [
{ "width": 100, "url": "https://d1zdv6x2we68mp.cloudfront.net/files/variants/public/.../photo-k9x2m7_w100.webp" },
{ "width": 300, "url": "https://d1zdv6x2we68mp.cloudfront.net/files/variants/public/.../photo-k9x2m7_w300.webp" },
{ "width": 400, "url": "..." },
{ "width": 600, "url": "..." },
{ "width": 800, "url": "..." },
{ "width": 1000, "url": "..." },
{ "width": 1200, "url": "..." }
]
}
PATCH/api/v1/files/{id}

Update file visibility or owner. Changing visibility updates both the database and S3 object metadata.

Request Body
{
"visibility": "public", // Optional
"ownerUserId": "user_456" // Optional — reassign owner
}
Response
{
"file": { ... }
}
DELETE/api/v1/files/{id}

Deletes a single file — original, all variants from S3, database record, and decrements storage.

Example
curl -X DELETE https://xfiles.dev/api/v1/files/abc123def456 \
-H "Authorization: Bearer xf_live_your_api_key"
Response
{
"success": true
}
DELETE/api/v1/files

Bulk delete all files matching entity filters. Use this when deleting a parent entity (e.g. a product) to clean up all attached files in one call. At least one filter is required.

Example
curl -X DELETE "https://xfiles.dev/api/v1/files?entityType=product&entityId=prod_456" \
-H "Authorization: Bearer xf_live_your_api_key"
# With role filter — delete only gallery images, keep documents
curl -X DELETE "https://xfiles.dev/api/v1/files?entityType=product&entityId=prod_456&role=gallery" \
-H "Authorization: Bearer xf_live_your_api_key"
Response
{
"deleted": 5
}
POST/api/v1/files/{id}/url

Generate fresh URLs for file access. Returns a signed original URL, plus CDN variant URLs for images. Public variant URLs are permanent. Useful when signed URLs have expired (valid for 5 minutes).

Example
curl -X POST https://xfiles.dev/api/v1/files/abc123def456/url \
-H "Authorization: Bearer xf_live_your_api_key"
Response
{
"url": "https://d1zdv6x2we68mp.cloudfront.net/files/originals/...?Signature=...&Expires=...",
"variants": [
{ "width": 100, "url": "https://d1zdv6x2we68mp.cloudfront.net/files/variants/public/.../photo-k9x2m7_w100.webp" },
...
],
"expiresIn": 300
}
// For SVG files — svgUrl instead of variants:
{
"url": "https://d1zdv6x2we68mp.cloudfront.net/files/originals/...?Signature=...",
"svgUrl": "https://d1zdv6x2we68mp.cloudfront.net/files/variants/public/.../logo-a8b3c1.svg",
"expiresIn": 300
}
04

Upload Security

The presigned URL cryptographically locks the following fields. If the client sends different values, S3 rejects the upload with 403 Forbidden.

Locked FieldWhat It Prevents
Content-TypeMust match the contentType declared at intent — S3 rejects mismatches
Content-LengthMust match the size declared at intent — prevents uploading larger files
x-amz-meta-visibilityCannot tamper with visibility flag
x-amz-meta-project-idCannot assign file to a different project
x-amz-meta-file-idCannot reassign to a different database record
ExpiryURL is valid for 15 minutes only

Two-step verification: After the client uploads, the confirm endpoint verifies the file actually exists in S3 before marking it as confirmed. This prevents orphaned database records from failed or abandoned uploads.

05

File Visibility

Core Rule

Original files are always protected and require a signed URL. Visibility controls access to image variants (resized raster images and SVG passthrough copies served via CDN).

 PublicPrivate (default)
Original fileSigned URL requiredSigned URL required
Image variants (raster)Open CDN access — no signing neededSigned URL required
SVG filesPermanent CDN URL — indexable by search enginesSigned URL required
Non-image filesAlways private — visibility setting does not apply

Changing visibility: Use PATCH /api/v1/files/{id} with {"visibility": "public"}. The change takes effect immediately for new variant requests.

06

Image Variants

Image variants are generated on demand the first time they're requested, then cached by the CDN. Raster images (JPEG, PNG, GIF, WebP) are resized and reformatted. SVG files are copied as-is to the variant path — they scale infinitely and don't need processing.

Variant URL patterns
# Raster images (resized + reformatted)
https://d1zdv6x2we68mp.cloudfront.net/files/variants/{visibility}/{projectId}/{fileId}/{baseName}_w{width}.{format}
# SVG files (passthrough — copied as-is, no resizing)
https://d1zdv6x2we68mp.cloudfront.net/files/variants/{visibility}/{projectId}/{fileId}/{baseName}.svg
Examples:
https://d1zdv6x2we68mp.cloudfront.net/files/variants/public/proj_abc/file_123/photo-k9x2m7_w400.webp
https://d1zdv6x2we68mp.cloudfront.net/files/variants/public/proj_abc/file_123/logo-a8b3c1.svg

Supported Widths

100px300px400px600px800px1000px1200px

Supported Formats

webpjpegjpgpngavif

Note: Public variants are served directly through the CDN without signing. Private variants require a signed URL — use the POST /api/v1/files/{id}/url endpoint to generate one.

SVG files: SVGs are vector graphics that scale infinitely — they skip resizing entirely. The CDN copies the original as-is to the variant path. Public SVGs get a permanent, indexable URL with no width parameter needed.

07

Filename Rules

Filenames you provide are sanitized before being used as storage keys. The original filename is preserved in the database for display.

#Rule
1Convert to lowercase
2Remove special characters (keep alphanumeric, spaces, hyphens)
3Replace spaces with hyphens
4Collapse multiple hyphens
5Preserve the original extension
6Append a unique 6-character suffix to avoid collisions
7Truncate base name to 100 characters max

Example

Input: "Best Ever Photo (2024).JPG"

Output: "best-ever-photo-2024-a7x9k2.jpg"

08

Content Types

You must declare the content type at upload intent time. Only whitelisted MIME types are accepted. The presigned URL locks both Content-Type and Content-Length so S3 rejects mismatches. Maximum file size: 100 MB.

Images

image/jpeg

image/png

image/gif

image/webp

image/svg+xml

image/avif

image/tiff

image/bmp

image/ico

Documents

application/pdf

application/msword

application/vnd.openxmlformats-*

text/plain

text/csv

text/html

text/css

text/javascript

application/json

application/xml

Archives

application/zip

application/gzip

application/x-tar

Audio

audio/mpeg

audio/wav

audio/ogg

audio/webm

Video

video/mp4

video/webm

video/ogg

Fonts

font/woff

font/woff2

font/ttf

font/otf

XFiles

File Management as a Service