Files Block

Enterprise cloud file storage with global CDN delivery
The Files Block provides a complete file management solution built on AWS S3 and CloudFront CDN. Upload files of any size with chunked uploads, transform images on-the-fly, generate presigned URLs for secure sharing, and serve files globally with sub-50ms latency.
Key Features
- AWS S3 Storage - Industry-leading object storage with 99.999999999% durability
- CloudFront CDN - 200+ edge locations for sub-50ms global delivery
- Chunked Uploads - Upload files up to 5GB with progress tracking and resume
- Image Transformations - Resize, crop, format conversion (WebP, AVIF) on-the-fly
- Presigned URLs - Time-limited secure access to private files
- Access Control - Public, private, and authenticated access modes
- Folder Organization - Virtual folders, metadata tagging, and full-text search
- Virus Scanning - Automatic malware detection on uploads
API Endpoint
| Service | URL |
|---|---|
| Files | https://files.api.us.23blocks.com |
Environment Routing: Use the same URL for all environments. Your API key determines the environment:
pk_test_*/sk_test_*→ Routes to Stagingpk_live_*/sk_live_*→ Routes to Production
Quick Start
Installation
npm install @23blocks/sdk
Basic Usage
import { create23BlocksClient } from '@23blocks/sdk';
const client = create23BlocksClient({
urls: { files: 'https://files.api.us.23blocks.com' },
apiKey: 'your-api-key',
});
// Upload a file
const file = await client.files.upload({
file: fileBlob,
folder: 'uploads/images',
public: true
});
// Get file URL with transformations
const thumbnailUrl = client.files.getUrl(file.id, {
width: 200,
height: 200,
format: 'webp'
});
// Generate presigned URL (expires in 1 hour)
const presignedUrl = await client.files.getPresignedUrl(file.id, {
expiresIn: 3600
});
Authentication
Required Headers
All API requests require authentication:
Authorization: Bearer [JWT_TOKEN]
X-API-Key: [COMPANY_API_KEY]
Content-Type: application/json
For file uploads, use multipart/form-data:
Content-Type: multipart/form-data
API Route Patterns
The Files API supports three types of file contexts, each with its own route structure:
User Files
Files owned by individual users:
| Method | Endpoint | Description |
|---|---|---|
| GET | /users/:unique_id/presign_upload | Get presigned URL for upload |
| POST | /users/:unique_id/files | Register file metadata |
| GET | /users/:unique_id/files | List user's files |
| PUT | /users/:unique_id/files/:unique_file_id | Update file metadata |
| POST | /users/:unique_id/multipart_upload | Initiate multipart upload |
| POST | /users/:unique_id/multipart_complete | Complete multipart upload |
Storage Files
Global storage files (not user-specific):
| Method | Endpoint | Description |
|---|---|---|
| GET | /storage/:url_id/presign_upload | Get presigned URL for upload |
| POST | /storage/:url_id/files | Register file metadata |
| GET | /storage/:url_id/files | List storage files |
| PUT | /storage/:url_id/files/:unique_file_id | Update file |
| DELETE | /storage/:url_id/files/:unique_file_id | Delete file |
Entity Files
Files attached to entities (CRM contacts, products, etc.):
| Method | Endpoint | Description |
|---|---|---|
| GET | /entities/:unique_id/presign | Get presigned URL for upload |
| POST | /entities/:unique_id/files/associate | Associate file with entity |
| GET | /entities/:unique_id/files | List entity's files |
| DELETE | /entities/:unique_id/files/:unique_file_id/disassociate | Remove file from entity |
Flat paths like /storage_files or /entity_files are NOT valid. Always use the nested route structure shown above.
Parameter Naming:
filenameis preferred, butfile_namealso works (backward compatibility)- Both parameters behave identically
API Reference
The Files API ALWAYS generates UUID-based S3 keys. The file_name returned from presign endpoints MUST be used in your metadata registration. Using any other value will cause 404 errors when downloading files.
Upload File (Single File)
File uploads use a 3-step process: get presigned URL → upload to S3 → register metadata.
Step 1: Get Presigned Upload URL
curl -X GET "https://files.api.us.23blocks.com/users/{user_unique_id}/presign_upload?filename=photo.jpg" \
-H "Authorization: Bearer your-jwt-token" \
-H "X-API-Key: your-api-key"
Response:
{
"data": {
"file_name": "dcca6ec1-3961-48d5-a1e3-54ce9dc197da.jpg",
"presigned_url": "https://s3.amazonaws.com/23blocks-files/..."
}
}
The file_name is a UUID-based S3 key. Save this value — you'll need it in Step 3.
Step 2: Upload File to S3
curl -X PUT "{presigned_url}" \
--upload-file /path/to/photo.jpg \
-H "Content-Type: image/jpeg"
Step 3: Register File Metadata
curl -X POST "https://files.api.us.23blocks.com/users/{user_unique_id}/files" \
-H "Authorization: Bearer your-jwt-token" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"name": "dcca6ec1-3961-48d5-a1e3-54ce9dc197da.jpg",
"original_name": "photo.jpg",
"file_type": "image/jpeg",
"file_size": 245678
}'
name MUST match the file_name from Step 1. This is the S3 key used for downloads.
Response:
{
"data": {
"id": "file_abc123",
"type": "file",
"attributes": {
"unique_id": "file_abc123",
"name": "dcca6ec1-3961-48d5-a1e3-54ce9dc197da.jpg",
"original_name": "photo.jpg",
"content_type": "image/jpeg",
"size": 245678,
"public": false,
"url": "https://cdn.23blocks.com/.../dcca6ec1-3961-48d5-a1e3-54ce9dc197da.jpg",
"created_at": "2024-01-15T10:30:00Z"
}
}
}
Field Explanations:
name- The S3 key (UUID). Used for downloads. NOT user-facing.original_name- The user's original filename. Used for display. This is what users see.
Chunked Upload (Large Files)
For files larger than 100MB, use multipart uploads with a 4-step process:
Step 1: Initiate Multipart Upload
curl -X POST "https://files.api.us.23blocks.com/users/{user_unique_id}/multipart_upload" \
-H "Authorization: Bearer your-jwt-token" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"filename": "large_video.mp4",
"file_type": "video/mp4",
"parts": 50
}'
Response:
{
"data": {
"file_name": "a1b2c3d4-5678-90ab-cdef-1234567890ab.mp4",
"upload_id": "xyz123...",
"presigned_urls": [
"https://s3.amazonaws.com/...?partNumber=1&uploadId=...",
"https://s3.amazonaws.com/...?partNumber=2&uploadId=..."
]
}
}
Save the file_name (UUID) — you'll need it in Steps 3 and 4.
Step 2: Upload Each Part
# Upload part 1
curl -X PUT "{presigned_urls[0]}" \
--upload-file chunk1.bin
# Upload part 2
curl -X PUT "{presigned_urls[1]}" \
--upload-file chunk2.bin
# ... continue for all parts
Save the ETag from each response — you'll need them in Step 3.
Step 3: Complete Multipart Upload
curl -X POST "https://files.api.us.23blocks.com/users/{user_unique_id}/multipart_complete" \
-H "Authorization: Bearer your-jwt-token" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"file_name": "a1b2c3d4-5678-90ab-cdef-1234567890ab.mp4",
"upload_id": "xyz123...",
"parts": [
{"ETag": "etag-from-part-1", "PartNumber": 1},
{"ETag": "etag-from-part-2", "PartNumber": 2}
]
}'
Step 4: Register File Metadata
curl -X POST "https://files.api.us.23blocks.com/users/{user_unique_id}/files" \
-H "Authorization: Bearer your-jwt-token" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"name": "a1b2c3d4-5678-90ab-cdef-1234567890ab.mp4",
"original_name": "large_video.mp4",
"file_type": "video/mp4",
"file_size": 524288000
}'
name MUST match the file_name from Step 1.
Common Mistake: Using Original Filename
This is the #1 cause of 404 errors in production:
# ❌ WRONG - This causes 404s
GET /presign_upload → Returns: { file_name: "dcca6ec1-3961-48d5-a1e3-54ce9dc197da.jpg" }
Upload to S3 with UUID key ✓
POST /files with { name: "my_original_file.jpg" } ← WRONG!
# Downloads fail with 404 because S3 key doesn't match
# ✅ CORRECT - This works
GET /presign_upload → Returns: { file_name: "dcca6ec1-3961-48d5-a1e3-54ce9dc197da.jpg" }
Upload to S3 with UUID key ✓
POST /files with {
name: "dcca6ec1-3961-48d5-a1e3-54ce9dc197da.jpg", ← Matches S3 key
original_name: "my_original_file.jpg" ← User-facing name
}
# Downloads work because name matches the actual S3 key
Why This Matters:
- File downloads generate presigned URLs using the
namefield - If
namedoesn't match the actual S3 key, downloads return 404 - RAG, AI pipelines, and all downstream services depend on this being correct
Get File
Retrieve file metadata:
curl -X GET https://files.api.us.23blocks.com/files/file_abc123 \
-H "Authorization: Bearer your-jwt-token" \
-H "X-API-Key: your-api-key"
List Files
List files in a folder:
curl -X GET "https://files.api.us.23blocks.com/files?folder=uploads/images&limit=50&offset=0" \
-H "Authorization: Bearer your-jwt-token" \
-H "X-API-Key: your-api-key"
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
folder | string | Filter by folder path |
content_type | string | Filter by MIME type (e.g., image/*) |
search | string | Full-text search in filenames and metadata |
limit | integer | Max results (default: 20, max: 100) |
offset | integer | Pagination offset |
sort | string | Sort field (created_at, size, filename) |
order | string | Sort order (asc, desc) |
Delete File
curl -X DELETE https://files.api.us.23blocks.com/files/file_abc123 \
-H "Authorization: Bearer your-jwt-token" \
-H "X-API-Key: your-api-key"
Update File Metadata
curl -X PATCH https://files.api.us.23blocks.com/files/file_abc123 \
-H "Authorization: Bearer your-jwt-token" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"metadata": {
"alt": "Updated description",
"tags": ["profile", "user"]
},
"public": false
}'
Presigned URLs
Generate temporary URLs for private file access:
curl -X POST https://files.api.us.23blocks.com/files/file_abc123/presign \
-H "Authorization: Bearer your-jwt-token" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"expires_in": 3600,
"disposition": "inline"
}'
Response:
{
"data": {
"url": "https://cdn.23blocks.com/company/private/file.pdf?X-Amz-Signature=...",
"expires_at": "2024-01-15T11:30:00Z"
}
}
Parameters:
| Parameter | Type | Description |
|---|---|---|
expires_in | integer | Seconds until URL expires (max: 604800 = 7 days) |
disposition | string | inline (view in browser) or attachment (force download) |
filename | string | Override download filename |
Image Transformations
Transform images on-the-fly by adding parameters to the URL:
https://cdn.23blocks.com/company/images/photo.jpg?w=300&h=200&f=webp&q=80
Transform Parameters
| Parameter | Description | Example |
|---|---|---|
w / width | Width in pixels | w=300 |
h / height | Height in pixels | h=200 |
f / format | Output format (webp, avif, png, jpg) | f=webp |
q / quality | Quality 1-100 (default: 85) | q=80 |
fit | Resize mode: cover, contain, fill, inside, outside | fit=cover |
position | Crop position: top, right, bottom, left, center | position=center |
blur | Gaussian blur 0.3-1000 | blur=5 |
sharpen | Sharpen filter | sharpen=1 |
grayscale | Convert to grayscale | grayscale=true |
rotate | Rotate degrees (90, 180, 270) | rotate=90 |
flip | Flip image (h = horizontal, v = vertical) | flip=h |
Transform Examples
Thumbnail (200x200, cropped to center, WebP):
https://cdn.23blocks.com/.../image.jpg?w=200&h=200&fit=cover&f=webp
Responsive hero image (max 1200px wide):
https://cdn.23blocks.com/.../image.jpg?w=1200&q=85&f=webp
Profile avatar (circular crop):
https://cdn.23blocks.com/.../image.jpg?w=100&h=100&fit=cover&position=center
SDK Transform Example
// Generate transformed image URL
const thumbnailUrl = client.files.getUrl('file_abc123', {
width: 200,
height: 200,
fit: 'cover',
format: 'webp',
quality: 80
});
// Returns: https://cdn.23blocks.com/.../image.jpg?w=200&h=200&fit=cover&f=webp&q=80
Folder Operations
Create Folder
curl -X POST https://files.api.us.23blocks.com/folders \
-H "Authorization: Bearer your-jwt-token" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"path": "uploads/2024/january",
"metadata": {
"description": "January 2024 uploads"
}
}'
List Folders
curl -X GET https://files.api.us.23blocks.com/folders?parent=uploads \
-H "Authorization: Bearer your-jwt-token" \
-H "X-API-Key: your-api-key"
Delete Folder
curl -X DELETE https://files.api.us.23blocks.com/folders/uploads%2F2024%2Fjanuary \
-H "Authorization: Bearer your-jwt-token" \
-H "X-API-Key: your-api-key"
Note: Deleting a folder also deletes all files within it.
Access Control
Visibility Levels
| Level | Description |
|---|---|
public | Anyone can access via CDN URL |
private | Requires presigned URL or authentication |
authenticated | Requires valid JWT token |
Set File Visibility
curl -X PATCH https://files.api.us.23blocks.com/files/file_abc123 \
-H "Authorization: Bearer your-jwt-token" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"public": false}'
Data Types
File
| Field | Type | Description |
|---|---|---|
unique_id | string | Unique identifier |
filename | string | Storage filename (may be sanitized) |
original_filename | string | Original upload filename |
content_type | string | MIME type |
size | integer | File size in bytes |
folder | string | Folder path |
public | boolean | Public access enabled |
url | string | CDN URL (for public files) |
metadata | object | Custom metadata key-value pairs |
dimensions | object | Width/height for images |
created_at | string | ISO 8601 timestamp |
updated_at | string | ISO 8601 timestamp |
Folder
| Field | Type | Description |
|---|---|---|
path | string | Full folder path |
name | string | Folder name |
parent | string | Parent folder path |
file_count | integer | Number of files |
total_size | integer | Total size in bytes |
Error Handling
The API uses JSON:API error format:
{
"errors": [
{
"status": "413",
"source": "Files Service",
"code": "20001",
"title": "File Too Large",
"detail": "File exceeds maximum size of 5GB"
}
]
}
Common Error Codes
| Status | Code | Description |
|---|---|---|
| 400 | 20001 | Invalid file format |
| 400 | 20002 | Invalid transform parameters |
| 401 | 20003 | Authentication required |
| 403 | 20004 | Access denied to file |
| 404 | 20005 | File not found |
| 409 | 20006 | File already exists |
| 413 | 20007 | File too large |
| 415 | 20008 | Unsupported media type |
| 422 | 20009 | Virus detected in upload |
| 429 | 20010 | Rate limit exceeded |
| 507 | 20011 | Storage quota exceeded |
Rate Limiting
| Endpoint Type | Limit |
|---|---|
| Upload | 100 requests/minute/API key |
| Download | 10,000 requests/minute/API key |
| Transformations | 5,000 requests/minute/API key |
| Presigned URLs | 500 requests/minute/API key |
Rate limit headers in responses:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1642248000
Storage & Bandwidth
Limits by Plan
| Plan | Storage | Bandwidth/month | Max File Size |
|---|---|---|---|
| Free | 5 GB | 10 GB | 100 MB |
| Starter | 50 GB | 100 GB | 1 GB |
| Pro | 500 GB | 1 TB | 5 GB |
| Enterprise | Unlimited | Custom | 5 GB |
Check Usage
curl -X GET https://files.api.us.23blocks.com/usage \
-H "Authorization: Bearer your-jwt-token" \
-H "X-API-Key: your-api-key"
Response:
{
"data": {
"storage_used": 1073741824,
"storage_limit": 5368709120,
"bandwidth_used": 2147483648,
"bandwidth_limit": 10737418240,
"file_count": 1234
}
}
SDK Examples
TypeScript/JavaScript
import { create23BlocksClient } from '@23blocks/sdk';
const client = create23BlocksClient({
urls: { files: 'https://files.api.us.23blocks.com' },
apiKey: process.env.BLOCKS_API_KEY,
});
// Upload with progress tracking
const file = await client.files.upload({
file: fileInput,
folder: 'uploads/documents',
public: false,
metadata: {
uploadedBy: 'user123',
category: 'contracts'
},
onProgress: (progress) => {
console.log(`Upload: ${progress}%`);
}
});
// List files in folder
const files = await client.files.list({
folder: 'uploads/documents',
contentType: 'application/pdf',
limit: 20
});
// Get file with transforms
const imageUrl = client.files.getUrl(file.id, {
width: 800,
format: 'webp',
quality: 85
});
// Generate presigned URL
const downloadUrl = await client.files.getPresignedUrl(file.id, {
expiresIn: 3600,
disposition: 'attachment',
filename: 'contract.pdf'
});
// Delete file
await client.files.delete(file.id);
React Component Example
import { useFiles, useUpload } from '@23blocks/react';
function FileUploader() {
const { upload, progress, isUploading, error } = useUpload();
const { files, isLoading, refetch } = useFiles({ folder: 'uploads' });
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
await upload({
file,
folder: 'uploads',
public: true
});
refetch(); // Refresh file list
};
return (
<div>
<input type="file" onChange={handleUpload} disabled={isUploading} />
{isUploading && <progress value={progress} max="100" />}
{error && <p className="error">{error.message}</p>}
<ul>
{files.map(file => (
<li key={file.id}>
{file.filename} ({file.size} bytes)
</li>
))}
</ul>
</div>
);
}
Node.js Server Example
import { create23BlocksClient } from '@23blocks/sdk';
import express from 'express';
import multer from 'multer';
const app = express();
const upload = multer({ storage: multer.memoryStorage() });
const client = create23BlocksClient({
urls: { files: 'https://files.api.us.23blocks.com' },
apiKey: process.env.BLOCKS_API_KEY,
});
// Proxy upload endpoint
app.post('/upload', upload.single('file'), async (req, res) => {
try {
const file = await client.files.upload({
file: req.file.buffer,
filename: req.file.originalname,
contentType: req.file.mimetype,
folder: 'user-uploads',
public: false
});
res.json({ fileId: file.id, url: file.url });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Generate signed URL for download
app.get('/download/:fileId', async (req, res) => {
const url = await client.files.getPresignedUrl(req.params.fileId, {
expiresIn: 300, // 5 minutes
disposition: 'attachment'
});
res.redirect(url);
});
Webhooks
Subscribe to file events:
curl -X POST https://files.api.us.23blocks.com/webhooks \
-H "Authorization: Bearer your-jwt-token" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/files",
"events": ["file.uploaded", "file.deleted", "file.scanned"],
"secret": "your-webhook-secret"
}'
Webhook Events
| Event | Description |
|---|---|
file.uploaded | File upload completed |
file.deleted | File was deleted |
file.scanned | Virus scan completed |
file.processed | Image/video processing completed |
folder.created | New folder created |
folder.deleted | Folder deleted |
Webhook Payload
{
"event": "file.uploaded",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"file": {
"id": "file_abc123",
"filename": "document.pdf",
"size": 1048576,
"folder": "uploads"
}
},
"signature": "sha256=..."
}
Best Practices
1. Use Chunked Uploads for Large Files
For files over 100MB, always use multipart uploads to ensure reliable transfers and enable resume capability.
2. Leverage CDN Caching
Set appropriate cache headers and use versioned filenames for static assets to maximize CDN cache hit rates.
3. Optimize Images
Use image transformations to serve appropriately sized images. Request WebP format with Accept header negotiation.
4. Use Presigned URLs for Private Content
Generate short-lived presigned URLs (5-60 minutes) for secure file access instead of exposing private storage URLs.
5. Implement Virus Scanning Callbacks
Listen to the file.scanned webhook event before making uploaded files available to users.
Related Resources
- Marketing Page - Feature overview
- API Reference - Interactive API documentation
- Platform Status - Service uptime monitoring