API Documentation

Complete reference for the File API

Overview

The File API provides RESTful endpoints for managing site files and folders. All endpoints return JSON and support standard HTTP methods.

Base URL
https://attach.fast/api

Authentication

All API requests require authentication using your site's API key.

Bearer Token Authentication
Authorization: Bearer your-site-api-key
Replace your-site-api-key with your actual site API key found in your site settings.

Site Files API

Manage file uploads, retrieval, and metadata with automatic folder structure creation.

List Files

GET /api/files

Retrieve a paginated list of site files ordered by creation date (newest first).

Query Parameters
ParameterTypeDescriptionDefault
pageintegerPage number1
per_pageintegerItems per page (max 100)10
Response
{
  "files": [
    {
      "id": 123,
      "token": "my-image.png",
      "filename": "my-image.png",
      "content_type": "image/png",
      "file_size": 524288,
      "url": "https://...",
      "download_url": "https://...",
      "folder": {
        "id": 456,
        "name": "avatars"
      },
      "created_at": "2025-12-30T16:18:00Z",
      "updated_at": "2025-12-30T16:18:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 10,
    "total_count": 45,
    "total_pages": 5
  }
}
Get Single File

GET /api/files/:id

Retrieve details for a specific file.

Response
{
  "id": 123,
  "token": "my-image.png",
  "filename": "my-image.png",
  "content_type": "image/png",
  "file_size": 524288,
  "url": "https://...",
  "download_url": "https://...",
  "folder": {
    "id": 456,
    "name": "avatars"
  },
  "created_at": "2025-12-30T16:18:00Z",
  "updated_at": "2025-12-30T16:18:00Z"
}
Upload File

POST /api/files

Upload a new file with automatic folder creation and unique naming.

Request Body (multipart/form-data)
FieldTypeRequiredDescription
filefileYesThe file to upload
folderstringNoCustom folder path/filename (e.g., "users/avatars/user_1")
File Naming & Folder Structure
Automatic Folder Creation: Folders in the path are created automatically if they don't exist.

Use the folder parameter to organize files:

  • "image" - Root level file (extension added automatically)
  • "users/avatar" - Creates "users" folder, file named "avatar.ext"
  • "docs/reports/quarterly" - Creates nested folders "docs" → "reports"
Duplicate Handling: If a file with the same token exists, a counter is added (e.g., "image-1.png", "image-2.png").
Response (201 Created)
{
  "id": 124,
  "token": "users/avatar.png",
  "filename": "avatar.png",
  "content_type": "image/png",
  "file_size": 45678,
  "url": "https://...",
  "download_url": "https://...",
  "folder": {
    "id": 457,
    "name": "users"
  },
  "created_at": "2025-12-30T16:20:00Z",
  "updated_at": "2025-12-30T16:20:00Z"
}
Update File Token

PATCH /api/files/:id

Update the file's token (filename/path).

Request Body (JSON)
{
  "site_file": {
    "token": "new-filename.png"
  }
}
Response

Returns the updated file object with new token.

Delete File

DELETE /api/files/:id

Permanently delete a file and its data from storage.

Response

204 No Content on successful deletion.

Large Files API

Upload large files using chunked uploads for improved reliability and performance. Perfect for videos, archives, or any file over 5MB.

When to use: Use the Large Files API for files over 5MB or when you need resumable upload capabilities. The system will automatically combine chunks once all pieces are received.
⏱️ Time Limit: Chunked uploads must be completed within 24 hours of the first chunk upload. Incomplete uploads will be automatically cleaned up after this period to free up server storage.
Chunked Upload Process

POST /api/large_files

Upload file chunks sequentially. The server assembles chunks and creates the final file when all chunks are received.

Upload Flow
  1. Split your large file into chunks (recommended: 5MB each)
  2. Upload each chunk with the same unique key
  3. Server tracks progress and assembles file when complete
  4. Final file is created and uploaded to storage
Request Body (multipart/form-data)
FieldTypeRequiredDescription
filefileYesThe chunk data
chunkintegerYesChunk number (1, 2, 3, etc.)
total_sizeintegerYesTotal size of original file in bytes
keystringYesUnique identifier for this upload (must be same for all chunks)
folderstringNoTarget folder path (e.g., "videos/2025")
Response - Chunk Received (202 Accepted)
{
  "message": "Chunk received successfully",
  "status": "pending",
  "received_size": 10485760,
  "total_size": 52428800
}
Response - Upload Complete (201 Created)
{
  "message": "File upload completed successfully",
  "status": "complete"
}
Error Responses
// Missing required parameters (422 Unprocessable Entity)
{
  "errors": ["Missing chunk number"]
}

// Server error during assembly (500 Internal Server Error)
{
  "errors": ["Failed to create final file: [error details]"]
}
Implementation Examples
JavaScript Chunked Upload
async function uploadLargeFile(file, apiKey, options = {}) {
  const chunkSize = options.chunkSize || 5 * 1024 * 1024; // 5MB
  const uniqueKey = options.key || `${file.name}_${Date.now()}`;
  const folder = options.folder;
  
  const totalChunks = Math.ceil(file.size / chunkSize);
  
  for (let chunkNumber = 1; chunkNumber <= totalChunks; chunkNumber++) {
    const start = (chunkNumber - 1) * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);
    
    const formData = new FormData();
    formData.append('file', chunk);
    formData.append('chunk', chunkNumber.toString());
    formData.append('total_size', file.size.toString());
    formData.append('key', uniqueKey);
    if (folder) formData.append('folder', folder);
    
    const response = await fetch('/api/large_files', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`
      },
      body: formData
    });
    
    const result = await response.json();
    
    if (!response.ok) {
      throw new Error(result.errors.join(', '));
    }
    
    console.log(`Chunk ${chunkNumber}/${totalChunks} uploaded`);
    
    // Check if upload is complete
    if (result.status === 'complete') {
      console.log('Upload completed successfully!');
      return result;
    }
  }
}
cURL Example - Single Chunk
# Upload chunk 1 of 3
curl -X POST https://attach.fast/api/large_files \
  -H "Authorization: Bearer your-site-api-key" \
  -F "file=@chunk_001" \
  -F "chunk=1" \
  -F "total_size=52428800" \
  -F "key=my_video_2025.mp4" \
  -F "folder=videos/uploads"
Python Example
import requests
import os

def upload_large_file(file_path, api_key, chunk_size=5*1024*1024):
    file_size = os.path.getsize(file_path)
    file_name = os.path.basename(file_path)
    unique_key = f"{file_name}_{int(time.time())}"
    
    with open(file_path, 'rb') as f:
        chunk_number = 1
        
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
                
            files = {'file': chunk}
            data = {
                'chunk': chunk_number,
                'total_size': file_size,
                'key': unique_key,
                'folder': 'uploads'
            }
            
            response = requests.post(
                'https://attach.fast/api/large_files',
                headers={'Authorization': f'Bearer {api_key}'},
                files=files,
                data=data
            )
            
            result = response.json()
            
            if not response.ok:
                raise Exception(f"Upload failed: {result['errors']}")
                
            print(f"Chunk {chunk_number} uploaded")
            
            if result['status'] == 'complete':
                print("Upload completed!")
                return result
                
            chunk_number += 1
Best Practices
Chunk Size Recommendations
  • 5MB chunks - Good default for most use cases
  • 1-2MB chunks - For slower connections
  • 10MB chunks - For very fast connections and large files
  • Maximum: 100MB per chunk
Retry Logic
async function uploadChunkWithRetry(chunk, chunkNumber, totalSize, key, apiKey, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const result = await uploadChunk(chunk, chunkNumber, totalSize, key, apiKey);
      return result;
    } catch (error) {
      if (attempt === maxRetries) {
        throw error;
      }
      console.log(`Chunk ${chunkNumber} failed, retrying (${attempt}/${maxRetries})`);
      await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // Exponential backoff
    }
  }
}
Progress Tracking
function trackProgress(receivedSize, totalSize) {
  const percentage = (receivedSize / totalSize * 100).toFixed(1);
  const receivedMB = (receivedSize / 1024 / 1024).toFixed(1);
  const totalMB = (totalSize / 1024 / 1024).toFixed(1);
  
  console.log(`Progress: ${percentage}% (${receivedMB}MB / ${totalMB}MB)`);
  
  // Update UI progress bar
  updateProgressBar(percentage);
}
Important Notes:
  • Keep the same key for all chunks of the same file
  • Upload chunks sequentially for best performance
  • Handle network errors with retry logic
  • Files become visible only after all chunks are processed
  • Complete uploads within 24 hours - incomplete uploads are automatically cleaned up after this period
When to Use Which API
ScenarioRecommended APIReason
Files under 5MB /api/files Simpler, single request upload
Files over 5MB /api/large_files Better performance, progress tracking, resumability
Slow/unreliable connections /api/large_files Retry individual chunks instead of entire file
Need progress tracking /api/large_files Built-in progress reporting

Site Folders API

Manage hierarchical folder structures with closure tree implementation.

List Folders

GET /api/folders

Retrieve folders with filtering and pagination, ordered by name.

Query Parameters
ParameterTypeDescriptionExample
parent_idintegerGet children of specific folder?parent_id=123
rootbooleanGet only root-level folders?root=true
pageintegerPage number (default: 1)?page=2
per_pageintegerItems per page (max 100, default: 10)?per_page=25
Response
{
  "folders": [
    {
      "id": 456,
      "name": "avatars",
      "parent_id": 123,
      "parent": {
        "id": 123,
        "name": "users"
      },
      "children_count": 2,
      "files_count": 15,
      "path": ["users", "avatars"],
      "created_at": "2025-12-30T16:00:00Z",
      "updated_at": "2025-12-30T16:00:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 10,
    "total_count": 8,
    "total_pages": 1
  }
}
Get Single Folder

GET /api/folders/:id

Retrieve a specific folder with its children and files.

Response
{
  "id": 456,
  "name": "avatars",
  "parent_id": 123,
  "parent": {
    "id": 123,
    "name": "users"
  },
  "children_count": 2,
  "files_count": 15,
  "path": ["users", "avatars"],
  "children": [
    {
      "id": 789,
      "name": "thumbnails",
      "parent_id": 456,
      "children_count": 0,
      "files_count": 5
    }
  ],
  "files": [
    {
      "id": 101,
      "token": "user_1.png",
      "filename": "user_1.png"
    }
  ],
  "created_at": "2025-12-30T16:00:00Z",
  "updated_at": "2025-12-30T16:00:00Z"
}
Get Complete Folder Tree

GET /api/folders/tree

Get complete hierarchical folder structure starting from root folders.

Response
{
  "tree": [
    {
      "id": 123,
      "name": "users",
      "files_count": 5,
      "children": [
        {
          "id": 456,
          "name": "avatars",
          "files_count": 15,
          "children": [
            {
              "id": 789,
              "name": "thumbnails", 
              "files_count": 5,
              "children": []
            }
          ]
        }
      ]
    },
    {
      "id": 124,
      "name": "documents",
      "files_count": 8,
      "children": []
    }
  ]
}
Create Folder

POST /api/folders

Create a new folder at root level or under an existing parent.

Request Body (JSON)
{
  "site_folder": {
    "name": "new-folder",
    "parent_id": 123
  }
}

Omit parent_id to create a root-level folder.

Response (201 Created)
{
  "id": 457,
  "name": "new-folder",
  "parent_id": 123,
  "parent": {
    "id": 123,
    "name": "users"
  },
  "children_count": 0,
  "files_count": 0,
  "path": ["users", "new-folder"],
  "created_at": "2025-12-30T16:25:00Z",
  "updated_at": "2025-12-30T16:25:00Z"
}
Update Folder

PATCH /api/folders/:id

Update folder name or move to different parent.

Request Body (JSON)
{
  "site_folder": {
    "name": "renamed-folder",
    "parent_id": 124
  }
}
Response

Returns the updated folder object.

Delete Folder

DELETE /api/folders/:id

Delete an empty folder (no subfolders or files).

Restrictions:
  • Cannot delete folders containing files
  • Cannot delete folders with subfolders
Error Responses
// 422 Unprocessable Entity
{
  "errors": ["Cannot delete folder with subfolders"]
}

// or
{
  "errors": ["Cannot delete folder containing files"]
}
Success Response

204 No Content on successful deletion.

Code Examples

cURL Examples
Upload a file with folder structure
curl -X POST https://attach.fast/api/files \
  -H "Authorization: Bearer your-site-api-key" \
  -F "file=@/path/to/image.png" \
  -F "folder=users/avatars/user_1"
Upload file to root level
curl -X POST https://attach.fast/api/files \
  -H "Authorization: Bearer your-site-api-key" \
  -F "file=@/path/to/document.pdf"
List files with pagination
curl -X GET "https://attach.fast/api/files?page=1&per_page=20" \
  -H "Authorization: Bearer your-site-api-key"
Get specific file details
curl -X GET https://attach.fast/api/files/123 \
  -H "Authorization: Bearer your-site-api-key"
Create a folder
curl -X POST https://attach.fast/api/folders \
  -H "Authorization: Bearer your-site-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "site_folder": {
      "name": "new-folder",
      "parent_id": 123
    }
  }'
Get folder tree structure
curl -X GET https://attach.fast/api/folders/tree \
  -H "Authorization: Bearer your-site-api-key"
List root folders only
curl -X GET "https://attach.fast/api/folders?root=true" \
  -H "Authorization: Bearer your-site-api-key"
Delete a file
curl -X DELETE https://attach.fast/api/files/123 \
  -H "Authorization: Bearer your-site-api-key"
JavaScript Examples
Upload a file with error handling
async function uploadFile(file, folder = null) {
  const formData = new FormData();
  formData.append('file', file);
  if (folder) formData.append('folder', folder);

  try {
    const response = await fetch('/api/files', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer your-site-api-key'
      },
      body: formData
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.errors.join(', '));
    }

    return await response.json();
  } catch (error) {
    console.error('Upload failed:', error.message);
    throw error;
  }
}
Fetch files with pagination
async function fetchFiles(page = 1, perPage = 10) {
  const response = await fetch(`/api/files?page=${page}&per_page=${perPage}`, {
    headers: {
      'Authorization': 'Bearer your-site-api-key'
    }
  });
  
  return await response.json();
}
Create folder hierarchy
async function createFolder(name, parentId = null) {
  const response = await fetch('/api/folders', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer your-site-api-key',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      site_folder: {
        name: name,
        parent_id: parentId
      }
    })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.errors.join(', '));
  }

  return await response.json();
}
Get complete folder tree
async function getFolderTree() {
  const response = await fetch('/api/folders/tree', {
    headers: {
      'Authorization': 'Bearer your-site-api-key'
    }
  });
  
  const data = await response.json();
  return data.tree;
}
React File Upload Component
import React, { useState } from 'react';

function FileUpload({ apiKey, onSuccess, onError }) {
  const [uploading, setUploading] = useState(false);
  const [folder, setFolder] = useState('');

  const handleUpload = async (e) => {
    const file = e.target.files[0];
    if (!file) return;

    setUploading(true);
    
    try {
      const formData = new FormData();
      formData.append('file', file);
      if (folder) formData.append('folder', folder);

      const response = await fetch('/api/files', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${apiKey}`
        },
        body: formData
      });

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.errors.join(', '));
      }

      const result = await response.json();
      onSuccess(result);
      setFolder(''); // Reset folder input
    } catch (error) {
      onError(error.message);
    } finally {
      setUploading(false);
    }
  };

  return (
    <div>
      <input 
        type="text" 
        placeholder="Optional: folder/path/filename"
        value={folder}
        onChange={(e) => setFolder(e.target.value)}
      />
      <input 
        type="file" 
        onChange={handleUpload}
        disabled={uploading}
      />
      {uploading && <p>Uploading...</p>}
    </div>
  );
}

Error Handling

The API uses standard HTTP status codes and returns structured JSON error responses.

HTTP Status Codes
CodeMeaningWhen Used
200OKSuccessful GET, PATCH requests
201CreatedSuccessful file upload, folder creation
204No ContentSuccessful deletion
400Bad RequestMalformed request parameters
401UnauthorizedInvalid or missing API key
404Not FoundFile, folder, or endpoint not found
422Unprocessable EntityValidation errors, business logic violations
Standard Error Response Format
{
  "errors": [
    "File is required",
    "Token has already been taken"
  ]
}
Common Error Scenarios
File Upload Errors (422 Unprocessable Entity)
// Missing file
{
  "errors": ["File is required"]
}

// Duplicate token (if token conflicts)
{
  "errors": ["Token has already been taken"]
}

// Invalid file type (if validation exists)
{
  "errors": ["File type not allowed"]
}
Folder Deletion Errors (422 Unprocessable Entity)
// Folder contains subfolders
{
  "errors": ["Cannot delete folder with subfolders"]
}

// Folder contains files
{
  "errors": ["Cannot delete folder containing files"]
}
Authentication Errors (401 Unauthorized)
// Missing Authorization header
{
  "errors": ["Authorization required"]
}

// Invalid API key
{
  "errors": ["Invalid API key"]
}
Resource Not Found (404 Not Found)
// File or folder doesn't exist
{
  "errors": ["Record not found"]
}
Error Handling Best Practices
JavaScript Error Handling
async function handleApiCall(url, options) {
  try {
    const response = await fetch(url, options);
    
    // Check for HTTP errors
    if (!response.ok) {
      const errorData = await response.json();
      const errorMessage = errorData.errors 
        ? errorData.errors.join(', ')
        : `HTTP ${response.status}: ${response.statusText}`;
      throw new Error(errorMessage);
    }
    
    return await response.json();
  } catch (error) {
    if (error.name === 'TypeError') {
      throw new Error('Network error or API unavailable');
    }
    throw error; // Re-throw API errors
  }
}
Tip: Always check the response.ok status before trying to parse JSON, as error responses still contain valid JSON with error details.