Quick Start
Get files uploading in four steps. Create a project, request an upload URL, push the file directly to storage, 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_AbC12dEfGhIjKlMnOpQrStUvWxYz9012. This key is shown once — copy it immediately.
2Request an Upload URL
1curl -X POST https://xfiles.dev/api/v1/files/upload-intent \2 -H "Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012" \3 -H "Content-Type: application/json" \4 -d '{5 "filename": "photo.jpg",6 "contentType": "image/jpeg",7 "size": 204800,8 "visibility": "public",9 "ownerUserId": "user_123",10 "entity": {11 "type": "post",12 "id": "post_abc123",13 "role": "gallery",14 "position": 015 },16 "transformations": {17 "image": { "enabled": true, "visibility": "public" }18 }19 }'1{2 "fileId": "abc123def456",3 "uploadUrl": "https://storage.example.com/...?X-Amz-Signature=...",4 "key": "files/originals/public/proj_id/abc123def456/photo-k9x2m7.jpg",5 "expiresIn": 9006}3Upload the File
PUT the file body to the presigned URL. The Content-Type and Content-Length must match exactly what you declared in step 2 — the server will reject mismatches with 403.
1curl -X PUT "UPLOAD_URL_FROM_STEP_2" \2 -H "Content-Type: CONTENT_TYPE_FROM_STEP_2" \3 --data-binary @photo.jpg4Confirm the Upload
1curl -X POST https://xfiles.dev/api/v1/files/confirm \2 -H "Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012" \3 -H "Content-Type: application/json" \4 -d '{ "fileId": "abc123def456" }'1{2 "file": {3 "id": "abc123def456",4 "filename": "photo-k9x2m7.jpg",5 "originalFilename": "photo.jpg",6 "contentType": "image/jpeg",7 "size": 204800,8 "visibility": "public",9 "uploadStatus": "confirmed",10 "ownerUserId": "user_123",11 "entity": {12 "type": "post",13 "id": "post_abc123",14 "role": "gallery",15 "position": 016 },17 "transformations": {18 "image": { "enabled": true, "visibility": "public" }19 },20 "createdAt": "2026-03-12T10:00:00.000Z"21 }22}How it works: Files never pass through your application server. Uploads go directly to S3 via presigned URLs. Downloads are always served through CloudFront CDN — public files get permanent CDN URLs, private files get signed CDN URLs. Your server only handles the intent and confirmation steps.
Authentication
All /api/v1/* routes authenticate via Bearer token using project API keys.
1Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012| Format | xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012 — 35 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 pagination, entity attachments, and filters |
| GET | /api/v1/files/{id} | Get file details + CDN URLs |
| 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 CDN URLs |
/api/v1/files/upload-intentCreates a pending file record and returns a presigned upload URL. The presigned URL locks Content-Type and Content-Length so the client cannot upload a different file than declared.
1{2 "filename": "photo.jpg", // Required — original filename3 "contentType": "image/jpeg", // Required — locked in presigned URL4 "size": 204800, // Required — locked in presigned URL5 "visibility": "public", // Optional — default "private"6 "ownerUserId": "user_123", // Optional — track who uploaded7 "entity": { // Optional — attach to an entity8 "type": "post", // Polymorphic entity type9 "id": "post_abc123", // Polymorphic entity ID10 "role": "gallery", // Optional — avatar, logo, gallery, document, etc.11 "position": 0 // Optional — display order (0 = primary, default: 0)12 },13 "transformations": { // Optional — image variant behavior14 "image": {15 "enabled": true, // Enable image variant generation (default: false)16 "visibility": "public" // "public" or "private" (default: "private")17 }18 }19}1{2 "fileId": "abc123def456",3 "uploadUrl": "https://storage.example.com/...?X-Amz-Signature=...",4 "key": "files/originals/public/proj_id/abc123def456/photo-k9x2m7.jpg",5 "expiresIn": 9006}/api/v1/files/confirmVerifies the file exists in storage, updates status to confirmed, increments storage usage, and auto-creates a file attachment if entity was provided at upload.
1{2 "fileId": "abc123def456"3}1{2 "file": {3 "id": "abc123def456",4 "filename": "photo-k9x2m7.jpg",5 "originalFilename": "photo.jpg",6 "contentType": "image/jpeg",7 "size": 204800,8 "visibility": "public",9 "uploadStatus": "confirmed",10 "ownerUserId": "user_123",11 "entity": {12 "type": "post",13 "id": "post_abc123",14 "role": "gallery",15 "position": 016 },17 "transformations": {18 "image": { "enabled": true, "visibility": "public" }19 }20 }21}/api/v1/filesList all confirmed files for the authenticated project. No filters required — call with just your API key to get all files. Always returns 20 files per page. Every file includes its entity attachment (or null). Optional filters narrow the results. When filtering by entity, results are sorted by position ascending.
1# List all files — no filters needed2curl https://xfiles.dev/api/v1/files \3 -H "Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012"4
5# Page 26curl "https://xfiles.dev/api/v1/files?page=2" \7 -H "Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012"8
9# Filter by entity — e.g. all images for a product10curl "https://xfiles.dev/api/v1/files?entityType=product&entityId=prod_abc123&role=gallery" \11 -H "Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012"12
13# Combine filters — public files for a specific owner14curl "https://xfiles.dev/api/v1/files?visibility=public&ownerUserId=user_123" \15 -H "Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012"1{2 "files": [3 {4 "id": "abc123def456",5 "filename": "sneaker-front-k9x2m7.jpg",6 "originalFilename": "sneaker-front.jpg",7 "contentType": "image/jpeg",8 "size": 204800,9 "visibility": "public",10 "uploadStatus": "confirmed",11 "ownerUserId": "user_123",12 "entity": {13 "type": "product",14 "id": "prod_abc123",15 "role": "gallery",16 "position": 017 },18 "transformations": {19 "image": { "enabled": true, "visibility": "public" }20 },21 "createdAt": "2026-03-12T10:00:00.000Z"22 },23 {24 "id": "def456ghi789",25 "filename": "contract-2024-m3x7k.pdf",26 "originalFilename": "contract-2024.pdf",27 "contentType": "application/pdf",28 "size": 4100000,29 "visibility": "private",30 "uploadStatus": "confirmed",31 "ownerUserId": null,32 "entity": {33 "type": "order",34 "id": "ord_k4m91",35 "role": "invoice",36 "position": 037 },38 "transformations": {39 "image": { "enabled": false, "visibility": "private" }40 },41 "createdAt": "2026-03-12T09:30:00.000Z"42 }43 ],44 "total": 42,45 "page": 1,46 "totalPages": 347}/api/v1/files/{id}Returns full file details and CDN URLs. All files are served through CloudFront — public files get direct CDN URLs, private files get signed CDN URLs. Raster images with transforms enabled include resized variant URLs.
1curl https://xfiles.dev/api/v1/files/abc123def456 \2 -H "Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012"1{2 "file": {3 "id": "abc123def456",4 "filename": "photo-k9x2m7.jpg",5 "originalFilename": "photo.jpg",6 "contentType": "image/jpeg",7 "size": 204800,8 "visibility": "public",9 "uploadStatus": "confirmed",10 "ownerUserId": "user_123",11 "entity": {12 "type": "post",13 "id": "post_abc123",14 "role": "gallery",15 "position": 016 },17 "transformations": {18 "image": { "enabled": true, "visibility": "public" }19 }20 },21 "url": "https://cdn.example.com/files/originals/public/.../photo-k9x2m7.jpg",22 "variants": [23 { "width": 100, "url": "https://cdn.example.com/files/variants/public/.../photo-k9x2m7_w100.webp" },24 { "width": 300, "url": "https://cdn.example.com/files/variants/public/.../photo-k9x2m7_w300.webp" },25 { "width": 400, "url": "..." },26 { "width": 600, "url": "..." },27 { "width": 800, "url": "..." },28 { "width": 1000, "url": "..." },29 { "width": 1200, "url": "..." }30 ]31}/api/v1/files/{id}Update file visibility, variant settings, owner, or display position. Changing visibility moves the S3 original between public/private paths. Changing variant visibility purges existing variants so they regenerate at the correct path.
1{2 "visibility": "public", // Optional — original file access3 "ownerUserId": "user_456", // Optional — reassign owner4 "position": 2, // Optional — display order (0 = primary, max 10000)5 "transformations": { // Optional — image variant behavior6 "image": {7 "enabled": true, // Toggle variant generation on/off8 "visibility": "public" // "public" or "private" (default: "private")9 }10 }11}1{2 "file": { ... }3}/api/v1/files/{id}Deletes a single file — original, all variants, database record, and decrements storage.
1curl -X DELETE https://xfiles.dev/api/v1/files/abc123def456 \2 -H "Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012"1{2 "success": true3}/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.
1curl -X DELETE "https://xfiles.dev/api/v1/files?entityType=product&entityId=prod_456" \2 -H "Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012"3
4# With role filter — delete only gallery images, keep documents5curl -X DELETE "https://xfiles.dev/api/v1/files?entityType=product&entityId=prod_456&role=gallery" \6 -H "Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012"1{2 "deleted": 53}/api/v1/files/{id}/urlGenerate fresh CDN URLs for file access. All files are always served through CloudFront. Public files get permanent CDN URLs, private files get signed CDN URLs (valid for 5 minutes). Images with transforms enabled include variant URLs.
1curl -X POST https://xfiles.dev/api/v1/files/abc123def456/url \2 -H "Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012"1// Public file with public variants — permanent CDN URLs:2{3 "url": "https://cdn.example.com/files/originals/public/.../photo.jpg",4 "variants": [5 { "width": 100, "url": "https://cdn.example.com/files/variants/public/.../photo_w100.webp" },6 { "width": 300, "url": "..." },7 ...8 ]9}10
11// Private file with private variants — signed CDN URLs (5 min expiry):12{13 "url": "https://cdn.example.com/...?Signature=...&Expires=...",14 "variants": [15 { "width": 100, "url": "https://cdn.example.com/...?Signature=...&Expires=..." },16 ...17 ],18 "expiresIn": 30019}20
21// SVGs and non-image files — original URL only, no variants:22{23 "url": "https://cdn.example.com/...?Signature=...",24 "expiresIn": 30025}Upload Security
The presigned URL cryptographically locks the following fields. If the client sends different values, the server rejects the upload with 403 Forbidden.
| Locked Field | What It Prevents |
|---|---|
Content-Type | Must match the contentType declared at intent — server rejects mismatches |
Content-Length | Must match the size declared at intent — prevents uploading larger files |
Visibility | Cannot tamper with visibility flag |
Project ID | Cannot assign file to a different project |
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 storage before marking it as confirmed. This prevents orphaned database records from failed or abandoned uploads.
File Visibility
Independent Visibility
All files are always served through CloudFront CDN — never directly from S3. Original files and image variants have independent visibility. Public files get permanent CDN URLs. Private files get signed CDN URLs (5 min expiry). Use transformations.image.visibility to control variant access — either "public" or "private" (defaults to "private").
| Public | Private (default) | |
|---|---|---|
| Original file | Permanent CDN URL — no signing needed | Signed CDN URL (5 min expiry) |
| Image variants | Permanent CDN URL — no signing needed | Signed CDN URL (5 min expiry) |
Use Case: Photo Marketplace
Private high-res originals (paid downloads) with public preview variants (thumbnails for browsing). Set visibility: "private" and transformations.image.visibility: "public".
Changing visibility: Use PATCH /api/v1/files/{id} with visibility and/or transformations.image.visibility. Changing original visibility moves the S3 object between public/private paths. Changing variant visibility purges existing variants so they regenerate at the correct path.
Image Variants
Image variants are opt-in — set transformations.image.enabled: true at upload time or toggle it later via PATCH. When enabled, 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. SVGs are vector graphics that scale infinitely — they don't get variants and are always served as the original file. When not enabled, no variant URLs are returned and Lambda@Edge will not generate variants.
1# Raster images — resized + reformatted, available at predefined widths2https://cdn.example.com/.../{baseName}_w{width}.{format}3
4# You don't need to construct these URLs — the API returns them for you.5# Use GET /api/v1/files/{id} or POST /api/v1/files/{id}/urlSupported Widths
Supported Formats
CDN-only delivery: All files — originals and variants — are always served through CloudFront CDN, never directly from S3. Public variants get permanent CDN URLs. Private variants get signed CDN URLs — use POST /api/v1/files/{id}/url to generate fresh ones.
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
File Management as a Service