Introduction
The Fula API provides S3-compatible endpoints for decentralized object storage. Built on IPFS with client-side encryption using HPKE and BLAKE3.
Base URL
All API requests should be made to:
https://your-gateway.example.com
Features
- S3 Compatible - Use existing S3 tools and SDKs
- Decentralized - Data stored across IPFS network
- Encrypted - Client-side encryption with HPKE
- Verifiable - BLAKE3 hashes and Bao streaming verification
# Using AWS CLI
aws --endpoint-url http://localhost:9000 \
s3 ls
# Using cURL
curl -X GET http://localhost:9000/ \
-H "Authorization: Bearer YOUR_TOKEN"
Authentication
All API requests require authentication using a Bearer token in the Authorization header.
Request Headers
| Header | Required | Description |
|---|---|---|
Authorization |
Required | Bearer token: Bearer <token> |
Permissions
Tokens include permissions that control access:
read- Read objects and list bucketswrite- Create and modify objectsdelete- Delete objects and buckets
curl -X GET http://localhost:9000/my-bucket/file.txt \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>Invalid or missing token</Message>
<RequestId>abc123</RequestId>
</Error>
Error Handling
Errors are returned as XML responses following S3 conventions.
Error Codes
| Code | HTTP Status | Description |
|---|---|---|
NoSuchBucket | 404 | Bucket does not exist |
NoSuchKey | 404 | Object does not exist |
BucketAlreadyExists | 409 | Bucket name taken |
AccessDenied | 403 | Permission denied |
InvalidArgument | 400 | Invalid request parameter |
InternalError | 500 | Server error |
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>NoSuchBucket</Code>
<Message>The specified bucket does not exist</Message>
<BucketName>my-bucket</BucketName>
<RequestId>4442587FB7D0A2F9</RequestId>
</Error>
Buckets
List Buckets
Returns a list of all buckets owned by the authenticated user.
Endpoint
GET /
Response
| Field | Type | Description |
|---|---|---|
Buckets | Array | List of bucket objects |
Bucket.Name | String | Bucket name |
Bucket.CreationDate | DateTime | ISO 8601 creation time |
curl -X GET http://localhost:9000/ \
-H "Authorization: Bearer $TOKEN"
<?xml version="1.0" encoding="UTF-8"?>
<ListAllMyBucketsResult>
<Owner>
<ID>user123</ID>
<DisplayName>John Doe</DisplayName>
</Owner>
<Buckets>
<Bucket>
<Name>my-documents</Name>
<CreationDate>2024-01-15T10:30:00Z</CreationDate>
</Bucket>
<Bucket>
<Name>backups</Name>
<CreationDate>2024-02-20T14:45:00Z</CreationDate>
</Bucket>
</Buckets>
</ListAllMyBucketsResult>
Create Bucket
Creates a new bucket with the specified name.
Endpoint
PUT /{bucket}
Path Parameters
| Parameter | Required | Description |
|---|---|---|
bucket |
Required | Bucket name (3-63 chars, lowercase, no spaces) |
Response Codes
200 OK- Bucket created successfully409 Conflict- Bucket already exists
curl -X PUT http://localhost:9000/my-new-bucket \
-H "Authorization: Bearer $TOKEN"
HTTP/1.1 200 OK
Location: /my-new-bucket
Content-Length: 0
Delete Bucket
Deletes an empty bucket. The bucket must be empty before deletion.
Endpoint
DELETE /{bucket}
Path Parameters
| Parameter | Required | Description |
|---|---|---|
bucket |
Required | Name of the bucket to delete |
curl -X DELETE http://localhost:9000/my-bucket \
-H "Authorization: Bearer $TOKEN"
HTTP/1.1 204 No Content
Head Bucket
Check if a bucket exists and you have permission to access it.
Endpoint
HEAD /{bucket}
curl -I http://localhost:9000/my-bucket \
-H "Authorization: Bearer $TOKEN"
HTTP/1.1 200 OK
x-amz-bucket-region: us-east-1
Content-Length: 0
Objects
List Objects (v2)
Returns objects in a bucket. Supports pagination and prefix filtering.
Endpoint
GET /{bucket}?list-type=2
Query Parameters
| Parameter | Required | Description |
|---|---|---|
list-type | Optional | Use 2 for ListObjectsV2 |
prefix | Optional | Filter by key prefix |
delimiter | Optional | Group keys by delimiter |
max-keys | Optional | Max results (default: 1000) |
continuation-token | Optional | Pagination token |
curl -X GET "http://localhost:9000/my-bucket?list-type=2&prefix=docs/&max-keys=10" \
-H "Authorization: Bearer $TOKEN"
<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult>
<Name>my-bucket</Name>
<Prefix>docs/</Prefix>
<MaxKeys>10</MaxKeys>
<IsTruncated>false</IsTruncated>
<Contents>
<Key>docs/readme.md</Key>
<LastModified>2024-03-15T10:30:00Z</LastModified>
<ETag>"d41d8cd98f00b204e9800998ecf8427e"</ETag>
<Size>1024</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
<Contents>
<Key>docs/api.md</Key>
<LastModified>2024-03-16T14:20:00Z</LastModified>
<ETag>"098f6bcd4621d373cade4e832627b4f6"</ETag>
<Size>2048</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
</ListBucketResult>
Get Object
Retrieves an object from a bucket. Supports range requests and conditional headers.
Endpoint
GET /{bucket}/{key}
Request Headers
| Header | Description |
|---|---|
Range | Byte range (e.g., bytes=0-1023) |
If-None-Match | Return 304 if ETag matches |
If-Modified-Since | Return 304 if not modified |
Response Headers
| Header | Description |
|---|---|
ETag | Object's MD5 hash |
Content-Length | Size in bytes |
Content-Type | MIME type |
Last-Modified | Last modification time |
x-amz-meta-* | Custom metadata |
# Full object
curl -X GET http://localhost:9000/my-bucket/file.txt \
-H "Authorization: Bearer $TOKEN" \
-o file.txt
# Range request (first 1KB)
curl -X GET http://localhost:9000/my-bucket/large-file.bin \
-H "Authorization: Bearer $TOKEN" \
-H "Range: bytes=0-1023"
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 1024
ETag: "d41d8cd98f00b204e9800998ecf8427e"
Last-Modified: Sat, 15 Mar 2024 10:30:00 GMT
x-amz-meta-custom: my-value
[file contents]
Put Object
Uploads an object to a bucket. Objects are content-addressed using BLAKE3.
Endpoint
PUT /{bucket}/{key}
Request Headers
| Header | Description |
|---|---|
Content-Type | MIME type of the object |
Content-Length | Size in bytes |
Content-MD5 | Base64 MD5 for verification |
x-amz-meta-* | Custom metadata headers |
# Upload a file
curl -X PUT http://localhost:9000/my-bucket/document.pdf \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/pdf" \
-H "x-amz-meta-author: John Doe" \
--data-binary @document.pdf
# Upload text content
curl -X PUT http://localhost:9000/my-bucket/hello.txt \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: text/plain" \
-d "Hello, World!"
HTTP/1.1 200 OK
ETag: "098f6bcd4621d373cade4e832627b4f6"
x-amz-version-id: v1.0
Delete Object
Removes an object from a bucket.
Endpoint
DELETE /{bucket}/{key}
curl -X DELETE http://localhost:9000/my-bucket/old-file.txt \
-H "Authorization: Bearer $TOKEN"
HTTP/1.1 204 No Content
x-amz-delete-marker: true
Copy Object
Creates a copy of an object. Can copy within or across buckets.
Endpoint
PUT /{bucket}/{key}
Request Headers
| Header | Required | Description |
|---|---|---|
x-amz-copy-source |
Required | Source: /bucket/key |
curl -X PUT http://localhost:9000/backup-bucket/file-copy.txt \
-H "Authorization: Bearer $TOKEN" \
-H "x-amz-copy-source: /my-bucket/original-file.txt"
<?xml version="1.0" encoding="UTF-8"?>
<CopyObjectResult>
<LastModified>2024-03-20T15:30:00Z</LastModified>
<ETag>"098f6bcd4621d373cade4e832627b4f6"</ETag>
</CopyObjectResult>
Head Object
Retrieves metadata without the object body.
Endpoint
HEAD /{bucket}/{key}
curl -I http://localhost:9000/my-bucket/file.txt \
-H "Authorization: Bearer $TOKEN"
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 1024
ETag: "d41d8cd98f00b204e9800998ecf8427e"
Last-Modified: Sat, 15 Mar 2024 10:30:00 GMT
x-amz-meta-author: John Doe
Multipart Upload
Upload large files in parts for improved reliability and performance.
Initiate Multipart Upload
Starts a multipart upload and returns an upload ID.
Endpoint
POST /{bucket}/{key}?uploads
curl -X POST "http://localhost:9000/my-bucket/large-file.zip?uploads" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/zip"
<?xml version="1.0" encoding="UTF-8"?>
<InitiateMultipartUploadResult>
<Bucket>my-bucket</Bucket>
<Key>large-file.zip</Key>
<UploadId>VXBsb2FkIElE-abc123xyz</UploadId>
</InitiateMultipartUploadResult>
Upload Part
Uploads a part of a multipart upload. Parts must be at least 5MB (except last).
Endpoint
PUT /{bucket}/{key}?partNumber={n}&uploadId={id}
Query Parameters
| Parameter | Required | Description |
|---|---|---|
partNumber | Required | Part number (1-10000) |
uploadId | Required | Upload ID from initiate |
# Upload part 1
curl -X PUT "http://localhost:9000/my-bucket/large-file.zip?partNumber=1&uploadId=VXBsb2FkIElE-abc123xyz" \
-H "Authorization: Bearer $TOKEN" \
--data-binary @part1.bin
HTTP/1.1 200 OK
ETag: "a54357aff0632cce46d942af68356b38"
Complete Multipart Upload
Completes a multipart upload by assembling uploaded parts.
Endpoint
POST /{bucket}/{key}?uploadId={id}
curl -X POST "http://localhost:9000/my-bucket/large-file.zip?uploadId=VXBsb2FkIElE-abc123xyz" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/xml" \
-d '<CompleteMultipartUpload>
<Part><PartNumber>1</PartNumber><ETag>"a54357aff0632cce46d942af68356b38"</ETag></Part>
<Part><PartNumber>2</PartNumber><ETag>"0dc9f8eb2b9d9a6d8f8c7e5b4a3c2d1e"</ETag></Part>
</CompleteMultipartUpload>'
<?xml version="1.0" encoding="UTF-8"?>
<CompleteMultipartUploadResult>
<Location>http://localhost:9000/my-bucket/large-file.zip</Location>
<Bucket>my-bucket</Bucket>
<Key>large-file.zip</Key>
<ETag>"17fbc0a106abbb6f381aac6e331f2a19-2"</ETag>
</CompleteMultipartUploadResult>
Abort Multipart Upload
Aborts a multipart upload and frees associated resources.
Endpoint
DELETE /{bucket}/{key}?uploadId={id}
curl -X DELETE "http://localhost:9000/my-bucket/large-file.zip?uploadId=VXBsb2FkIElE-abc123xyz" \
-H "Authorization: Bearer $TOKEN"
HTTP/1.1 204 No Content
Object Tagging
Add key-value metadata tags to objects for categorization and lifecycle management.
Get Object Tagging
Returns the tag set associated with an object.
Endpoint
GET /{bucket}/{key}?tagging
curl -X GET "http://localhost:9000/my-bucket/file.txt?tagging" \
-H "Authorization: Bearer $TOKEN"
<?xml version="1.0" encoding="UTF-8"?>
<Tagging>
<TagSet>
<Tag>
<Key>environment</Key>
<Value>production</Value>
</Tag>
<Tag>
<Key>project</Key>
<Value>website</Value>
</Tag>
</TagSet>
</Tagging>
Put Object Tagging
Sets tags on an object. Replaces all existing tags.
Endpoint
PUT /{bucket}/{key}?tagging
Limits
- Maximum 10 tags per object
- Key: 1-128 Unicode characters
- Value: 0-256 Unicode characters
curl -X PUT "http://localhost:9000/my-bucket/file.txt?tagging" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/xml" \
-d '<Tagging>
<TagSet>
<Tag><Key>status</Key><Value>approved</Value></Tag>
<Tag><Key>owner</Key><Value>alice</Value></Tag>
</TagSet>
</Tagging>'
HTTP/1.1 200 OK
Content-Length: 0
Delete Object Tagging
Removes all tags from an object.
Endpoint
DELETE /{bucket}/{key}?tagging
curl -X DELETE "http://localhost:9000/my-bucket/file.txt?tagging" \
-H "Authorization: Bearer $TOKEN"
HTTP/1.1 204 No Content
🔐 Client-Side Encryption Headers
When using client-side encryption, Fula stores encryption metadata in custom headers.
Encryption Metadata Header
The x-amz-meta-encryption header contains JSON with encryption info:
Fields
- version - Encryption format version (currently 2)
- algorithm - Cipher used (AES-256-GCM)
- nonce - Base64-encoded nonce
- wrapped_key - HPKE-encrypted DEK
- metadata_privacy - Whether private metadata is included
- private_metadata - Encrypted original filename, size, etc.
{
"version": 2,
"algorithm": "AES-256-GCM",
"nonce": "base64_encoded_nonce",
"wrapped_key": {
"encapsulated_key": "base64_hpke_encapsulated_key",
"ciphertext": "base64_encrypted_dek",
"nonce": "base64_inner_nonce"
},
"metadata_privacy": true,
"private_metadata": "{\"version\":1,\"ciphertext\":\"...\",\"nonce\":\"...\"}"
}
# Get metadata without downloading content
curl -I "http://localhost:9000/my-bucket/e/a7c3f9b2e8d14a6f" \
-H "Authorization: Bearer $TOKEN"
# Response headers include:
# x-amz-meta-encrypted: true
# x-amz-meta-encryption: {"version":2,"algorithm":"AES-256-GCM",...}
# Content-Type: application/octet-stream
# Content-Length: 156821 (ciphertext size)
🔒 Metadata Privacy
With metadata privacy enabled, the server never sees your real filenames, sizes, or content types.
What Gets Obfuscated
| Field | Server Sees | Client Decrypts |
|---|---|---|
| Key (filename) | e/a7c3f9b2e8d14a6f |
/finances/tax_2024.pdf |
| Size | 156,821 (ciphertext) | 156,789 (original) |
| Content-Type | application/octet-stream |
application/pdf |
| Timestamps | Upload time only | Original created/modified |
Private Metadata Structure
The private_metadata field, when decrypted, contains:
{
"original_key": "/finances/tax_returns_2024.pdf",
"actual_size": 156789,
"content_type": "application/pdf",
"created_at": 1701388800,
"modified_at": 1701475200,
"user_metadata": {
"author": "John Doe",
"department": "Accounting"
},
"content_hash": "blake3_hash_of_plaintext"
}
# DeterministicHash (default)
# Same path → same storage key (allows retrieval)
/photos/beach.jpg → e/a7c3f9b2e8d14a6f
# RandomUuid
# Each upload gets random key (maximum privacy)
/photos/beach.jpg → e/550e8400-e29b-41d4-a716-446655440000
# PreserveStructure
# Keep folder paths, hash only filenames
/photos/beach.jpg → /photos/e_a7c3f9b2e8d1
🤝 Secure Sharing API
Share encrypted files without exposing your master key. Uses HPKE to re-encrypt the DEK for each recipient.
Share Token Structure
A share token is a JSON object containing:
- share_id - Unique identifier (random 16-byte hex)
- path_scope - Folder path this share grants access to
- expires_at - Unix timestamp when share expires (optional)
- permissions - Read, write, delete flags
- encrypted_dek - HPKE-encrypted DEK for recipient
- owner_public_key - Owner's public key (for verification)
Permissions
read_only()- Can decrypt and read filesread_write()- Can read and upload new filesfull_access()- Can read, write, and delete
{
"share_id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"path_scope": "/photos/vacation/",
"created_at": 1701388800,
"expires_at": 1702252800,
"permissions": {
"read": true,
"write": false,
"delete": false
},
"encrypted_dek": {
"encapsulated_key": "base64...",
"ciphertext": "base64...",
"nonce": "base64..."
},
"owner_public_key": "base64_x25519_public_key"
}
┌─────────────────────────────────────────────────────────────────┐
│ SECURE SHARING FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. OWNER creates share: │
│ • Specify path scope: /photos/vacation/ │
│ • Set expiry: 7 days │
│ • Set permissions: read-only │
│ • Re-encrypt DEK for recipient's public key │
│ │
│ 2. OWNER sends token to RECIPIENT: │
│ • Via email, message, QR code, etc. │
│ • Owner's private key never leaves device │
│ │
│ 3. RECIPIENT accepts share: │
│ • Verify token signature │
│ • Check expiry │
│ • Decrypt DEK with own private key │
│ │
│ 4. RECIPIENT accesses files: │
│ • Fetch encrypted files from server │
│ • Decrypt with the shared DEK │
│ • Path checked against scope │
│ │
└─────────────────────────────────────────────────────────────────┘