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
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" } }'{ "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.
curl -X PUT "UPLOAD_URL_FROM_STEP_2" \ -H "Content-Type: CONTENT_TYPE_FROM_STEP_2" \ --data-binary @photo.jpg4Confirm the Upload
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" }'{ "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.
Authentication
All /api/v1/* routes authenticate via Bearer token using project API keys.
Authorization: Bearer xf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| Format | xf_live_{nanoid(32)} — 38 characters total |
| Created via | Dashboard — New Project — key shown once, copy immediately |
| Rotation | Dashboard — Project Settings — Rotate Key — old key immediately invalidated |
| Errors | Missing or invalid key returns 401 Unauthorized |
API Reference
File EndpointsAPI Key Auth
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/files/upload-intent | Get presigned upload URL |
| POST | /api/v1/files/confirm | Confirm upload after S3 PUT |
| GET | /api/v1/files | List 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/files | Bulk delete by entity filters |
| POST | /api/v1/files/{id}/url | Generate fresh signed URL |
/api/v1/files/upload-intentCreates 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.
{ "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. }}{ "fileId": "abc123def456", "uploadUrl": "https://xfiles-storage.s3.amazonaws.com/...", "key": "files/originals/proj_id/abc123def456/photo-k9x2m7.jpg", "expiresIn": 900}/api/v1/files/confirmVerifies the file exists in S3, updates status to confirmed, increments storage usage, and auto-creates a file attachment if entity was provided at upload.
{ "fileId": "abc123def456"}{ "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" } }}/api/v1/filesList confirmed files for the authenticated project. Supports filtering and pagination.
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"{ "files": [ ... ], "total": 42, "page": 1, "limit": 20, "totalPages": 3}/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.
curl https://xfiles.dev/api/v1/files/abc123def456 \ -H "Authorization: Bearer xf_live_your_api_key"{ "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": "..." } ]}/api/v1/files/{id}Update file visibility or owner. Changing visibility updates both the database and S3 object metadata.
{ "visibility": "public", // Optional "ownerUserId": "user_456" // Optional — reassign owner}{ "file": { ... }}/api/v1/files/{id}Deletes a single file — original, all variants from S3, database record, and decrements storage.
curl -X DELETE https://xfiles.dev/api/v1/files/abc123def456 \ -H "Authorization: Bearer xf_live_your_api_key"{ "success": true}/api/v1/filesBulk 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.
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 documentscurl -X DELETE "https://xfiles.dev/api/v1/files?entityType=product&entityId=prod_456&role=gallery" \ -H "Authorization: Bearer xf_live_your_api_key"{ "deleted": 5}/api/v1/files/{id}/urlGenerate 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).
curl -X POST https://xfiles.dev/api/v1/files/abc123def456/url \ -H "Authorization: Bearer xf_live_your_api_key"{ "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}Upload Security
The presigned URL cryptographically locks the following fields. If the client sends different values, S3 rejects the upload with 403 Forbidden.
| Locked Field | What It Prevents |
|---|---|
Content-Type | Must match the contentType declared at intent — S3 rejects mismatches |
Content-Length | Must match the size declared at intent — prevents uploading larger files |
x-amz-meta-visibility | Cannot tamper with visibility flag |
x-amz-meta-project-id | Cannot assign file to a different project |
x-amz-meta-file-id | Cannot reassign to a different database record |
Expiry | URL 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.
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).
| Public | Private (default) | |
|---|---|---|
| Original file | Signed URL required | Signed URL required |
| Image variants (raster) | Open CDN access — no signing needed | Signed URL required |
| SVG files | Permanent CDN URL — indexable by search engines | Signed URL required |
| Non-image files | Always 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.
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.
# 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.webphttps://d1zdv6x2we68mp.cloudfront.net/files/variants/public/proj_abc/file_123/logo-a8b3c1.svgSupported Widths
Supported Formats
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.
Filename Rules
Filenames you provide are sanitized before being used as storage keys. The original filename is preserved in the database for display.
| # | Rule |
|---|---|
| 1 | Convert to lowercase |
| 2 | Remove special characters (keep alphanumeric, spaces, hyphens) |
| 3 | Replace spaces with hyphens |
| 4 | Collapse multiple hyphens |
| 5 | Preserve the original extension |
| 6 | Append a unique 6-character suffix to avoid collisions |
| 7 | Truncate base name to 100 characters max |
Example
Input: "Best Ever Photo (2024).JPG"
Output: "best-ever-photo-2024-a7x9k2.jpg"
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