XFiles

File infrastructure
that ships with your app

XFiles gives your application a complete file system — uploads, image resizing, CDN delivery, and polymorphic attachments — through a single REST API that works with any framework.

Every project needs file handling, and every team ends up building the same pipeline from scratch. We did the hard part so you can ship the rest.

Pay as you go

$0.05/GB/mo
$0.20/GB
Freetransforms

Prepaid creditsNo subscriptionTop up from $5

Direct-to-S3 uploadsCloudFront CDN delivery7 image variants on demandPublic + signed private URLsPolymorphic entity attachments
upload demo

Scenario

sneaker-front.jpg

image/jpeg · 2.4 MB

product/gallery

prod_8kx2m

File visibility
Image transforms
Variant visibility

Request body preview

{
  "filename": "sneaker-front.jpg",
  "visibility": "public",
  "entity": {
    "type": "product",
    "id": "prod_8kx2m",
    "role": "gallery"
  },
  "transformations": {
    "image": {
      "enabled": true,
      "visibility": "public"
    }
  }
}
See the API docs
01

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

POST /api/v1/files/upload-intent
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": 0
15 },
16 "transformations": {
17 "image": { "enabled": true, "visibility": "public" }
18 }
19 }'
Response
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": 900
6}

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.

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

4Confirm the Upload

POST /api/v1/files/confirm
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" }'
Response
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": 0
16 },
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.

02

Authentication

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

Authorization header
1Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012
Formatxf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012 — 35 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 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/filesBulk delete by entity filters
POST/api/v1/files/{id}/urlGenerate fresh CDN URLs
POST/api/v1/files/upload-intent

Creates 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.

Request Body
1{
2 "filename": "photo.jpg", // Required — original filename
3 "contentType": "image/jpeg", // Required — locked in presigned URL
4 "size": 204800, // Required — locked in presigned URL
5 "visibility": "public", // Optional — default "private"
6 "ownerUserId": "user_123", // Optional — track who uploaded
7 "entity": { // Optional — attach to an entity
8 "type": "post", // Polymorphic entity type
9 "id": "post_abc123", // Polymorphic entity ID
10 "role": "gallery", // Optional — avatar, logo, gallery, document, etc.
11 "position": 0 // Optional — display order (0 = primary, default: 0)
12 },
13 "transformations": { // Optional — image variant behavior
14 "image": {
15 "enabled": true, // Enable image variant generation (default: false)
16 "visibility": "public" // "public" or "private" (default: "private")
17 }
18 }
19}
Response
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": 900
6}
POST/api/v1/files/confirm

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

Request Body
1{
2 "fileId": "abc123def456"
3}
Response
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": 0
16 },
17 "transformations": {
18 "image": { "enabled": true, "visibility": "public" }
19 }
20 }
21}
GET/api/v1/files

List 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.

Example
1# List all files — no filters needed
2curl https://xfiles.dev/api/v1/files \
3 -H "Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012"
4
5# Page 2
6curl "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 product
10curl "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 owner
14curl "https://xfiles.dev/api/v1/files?visibility=public&ownerUserId=user_123" \
15 -H "Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012"
Response
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": 0
17 },
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": 0
37 },
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": 3
47}
GET/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.

Example
1curl https://xfiles.dev/api/v1/files/abc123def456 \
2 -H "Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012"
Response
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": 0
16 },
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}
PATCH/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.

Request Body
1{
2 "visibility": "public", // Optional — original file access
3 "ownerUserId": "user_456", // Optional — reassign owner
4 "position": 2, // Optional — display order (0 = primary, max 10000)
5 "transformations": { // Optional — image variant behavior
6 "image": {
7 "enabled": true, // Toggle variant generation on/off
8 "visibility": "public" // "public" or "private" (default: "private")
9 }
10 }
11}
Response
1{
2 "file": { ... }
3}
DELETE/api/v1/files/{id}

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

Example
1curl -X DELETE https://xfiles.dev/api/v1/files/abc123def456 \
2 -H "Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012"
Response
1{
2 "success": true
3}
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
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 documents
5curl -X DELETE "https://xfiles.dev/api/v1/files?entityType=product&entityId=prod_456&role=gallery" \
6 -H "Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012"
Response
1{
2 "deleted": 5
3}
POST/api/v1/files/{id}/url

Generate 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.

Example
1curl -X POST https://xfiles.dev/api/v1/files/abc123def456/url \
2 -H "Authorization: Bearer xf_AbC12dEfGhIjKlMnOpQrStUvWxYz9012"
Response
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": 300
19}
20
21// SVGs and non-image files — original URL only, no variants:
22{
23 "url": "https://cdn.example.com/...?Signature=...",
24 "expiresIn": 300
25}
04

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 FieldWhat It Prevents
Content-TypeMust match the contentType declared at intent — server rejects mismatches
Content-LengthMust match the size declared at intent — prevents uploading larger files
VisibilityCannot tamper with visibility flag
Project IDCannot assign file to a different project
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 storage before marking it as confirmed. This prevents orphaned database records from failed or abandoned uploads.

05

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").

 PublicPrivate (default)
Original filePermanent CDN URL — no signing neededSigned CDN URL (5 min expiry)
Image variantsPermanent CDN URL — no signing neededSigned 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.

06

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.

Variant URL structure
1# Raster images — resized + reformatted, available at predefined widths
2https://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}/url

Supported Widths

100px300px400px600px800px1000px1200px

Supported Formats

webpjpegjpgpngavif

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.

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: 500 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

Adobe

application/postscript

application/illustrator

image/vnd.adobe.photoshop

application/x-photoshop

application/x-indesign

application/vnd.adobe.indesign-idml-package

application/vnd.adobe.aftereffects.project

application/vnd.adobe.premiere

application/vnd.adobe.xd

image/x-adobe-dng

09

Billing & Credits

XFiles uses a prepaid credit balance. There's no subscription, no free tier, and no monthly invoice. You top up an amount, storage and bandwidth costs are debited as they accrue, and when your balance hits zero the API immediately stops accepting uploads and stops handing out signed CDN URLs. Files on disk are untouched — only serving pauses.

Storage

$0.05

per GB per month, debited daily

Bandwidth

$0.20

per GB transferred via CDN

Transforms

Free

7 image variant widths, on demand

Out-of-credit behaviour

Both POST /api/v1/files/upload-intent and POST /api/v1/files/{id}/url return 402 Payment Required the moment the balance drops to zero or below. Clients should treat 402 as a hard signal to prompt the user to top up — no retry will succeed until credits are added. Read-only file metadata endpoints (GET /api/v1/files, GET /api/v1/files/{id}) continue to respond, but the URLs they return will not resolve at the CDN.

Top-ups and low-balance alerts

Adding credits is a one-shot Stripe Checkout in mode=payment — no card is stored, no recurring charge is created. Pick an amount on /dashboard/billing, pay, and the balance updates as soon as Stripe's webhook fires. Stripe's hosted receipt URL is preserved with the transaction for accounting.

Each account has a configurable low-balance threshold (default $5.00). The first time your balance drops at or below that line, you get one warning email. The flag clears the next time you top up, so the alert can fire again for the next cycle.

Error codes

StatusMeaning
400Invalid request — missing fields, disallowed content type, file too large
401Missing or invalid API key
402Out of credits — top up at /dashboard/billing to resume uploads and CDN serving
404File not found
500Server error — safe to retry with backoff
XFiles

File Management as a Service