Skip to main content

Files Block

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

ServiceURL
Fileshttps://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 Staging
  • pk_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:

MethodEndpointDescription
GET/users/:unique_id/presign_uploadGet presigned URL for upload
POST/users/:unique_id/filesRegister file metadata
GET/users/:unique_id/filesList user's files
PUT/users/:unique_id/files/:unique_file_idUpdate file metadata
POST/users/:unique_id/multipart_uploadInitiate multipart upload
POST/users/:unique_id/multipart_completeComplete multipart upload

Storage Files

Global storage files (not user-specific):

MethodEndpointDescription
GET/storage/:url_id/presign_uploadGet presigned URL for upload
POST/storage/:url_id/filesRegister file metadata
GET/storage/:url_id/filesList storage files
PUT/storage/:url_id/files/:unique_file_idUpdate file
DELETE/storage/:url_id/files/:unique_file_idDelete file

Entity Files

Files attached to entities (CRM contacts, products, etc.):

MethodEndpointDescription
GET/entities/:unique_id/presignGet presigned URL for upload
POST/entities/:unique_id/files/associateAssociate file with entity
GET/entities/:unique_id/filesList entity's files
DELETE/entities/:unique_id/files/:unique_file_id/disassociateRemove file from entity
warning

Flat paths like /storage_files or /entity_files are NOT valid. Always use the nested route structure shown above.

Parameter Naming:

  • filename is preferred, but file_name also works (backward compatibility)
  • Both parameters behave identically

API Reference

CRITICAL: File Upload Process

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/..."
}
}
warning

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
}'
Required Field

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=..."
]
}
}
warning

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
}'
Required Field

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 name field
  • If name doesn'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:

ParameterTypeDescription
folderstringFilter by folder path
content_typestringFilter by MIME type (e.g., image/*)
searchstringFull-text search in filenames and metadata
limitintegerMax results (default: 20, max: 100)
offsetintegerPagination offset
sortstringSort field (created_at, size, filename)
orderstringSort 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:

ParameterTypeDescription
expires_inintegerSeconds until URL expires (max: 604800 = 7 days)
dispositionstringinline (view in browser) or attachment (force download)
filenamestringOverride 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

ParameterDescriptionExample
w / widthWidth in pixelsw=300
h / heightHeight in pixelsh=200
f / formatOutput format (webp, avif, png, jpg)f=webp
q / qualityQuality 1-100 (default: 85)q=80
fitResize mode: cover, contain, fill, inside, outsidefit=cover
positionCrop position: top, right, bottom, left, centerposition=center
blurGaussian blur 0.3-1000blur=5
sharpenSharpen filtersharpen=1
grayscaleConvert to grayscalegrayscale=true
rotateRotate degrees (90, 180, 270)rotate=90
flipFlip 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

LevelDescription
publicAnyone can access via CDN URL
privateRequires presigned URL or authentication
authenticatedRequires 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

FieldTypeDescription
unique_idstringUnique identifier
filenamestringStorage filename (may be sanitized)
original_filenamestringOriginal upload filename
content_typestringMIME type
sizeintegerFile size in bytes
folderstringFolder path
publicbooleanPublic access enabled
urlstringCDN URL (for public files)
metadataobjectCustom metadata key-value pairs
dimensionsobjectWidth/height for images
created_atstringISO 8601 timestamp
updated_atstringISO 8601 timestamp

Folder

FieldTypeDescription
pathstringFull folder path
namestringFolder name
parentstringParent folder path
file_countintegerNumber of files
total_sizeintegerTotal 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

StatusCodeDescription
40020001Invalid file format
40020002Invalid transform parameters
40120003Authentication required
40320004Access denied to file
40420005File not found
40920006File already exists
41320007File too large
41520008Unsupported media type
42220009Virus detected in upload
42920010Rate limit exceeded
50720011Storage quota exceeded

Rate Limiting

Endpoint TypeLimit
Upload100 requests/minute/API key
Download10,000 requests/minute/API key
Transformations5,000 requests/minute/API key
Presigned URLs500 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

PlanStorageBandwidth/monthMax File Size
Free5 GB10 GB100 MB
Starter50 GB100 GB1 GB
Pro500 GB1 TB5 GB
EnterpriseUnlimitedCustom5 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

EventDescription
file.uploadedFile upload completed
file.deletedFile was deleted
file.scannedVirus scan completed
file.processedImage/video processing completed
folder.createdNew folder created
folder.deletedFolder 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.