Fula API

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
cURL
# 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

HeaderRequiredDescription
Authorization Required Bearer token: Bearer <token>

Permissions

Tokens include permissions that control access:

  • read - Read objects and list buckets
  • write - Create and modify objects
  • delete - Delete objects and buckets
Request
curl -X GET http://localhost:9000/my-bucket/file.txt \
    -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
401 Unauthorized
<?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

CodeHTTP StatusDescription
NoSuchBucket404Bucket does not exist
NoSuchKey404Object does not exist
BucketAlreadyExists409Bucket name taken
AccessDenied403Permission denied
InvalidArgument400Invalid request parameter
InternalError500Server error
Error Response
<?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

GET

List Buckets

Returns a list of all buckets owned by the authenticated user.

Endpoint

GET /

Response

FieldTypeDescription
BucketsArrayList of bucket objects
Bucket.NameStringBucket name
Bucket.CreationDateDateTimeISO 8601 creation time
Request
curl -X GET http://localhost:9000/ \
    -H "Authorization: Bearer $TOKEN"
Response 200 OK
<?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>
PUT

Create Bucket

Creates a new bucket with the specified name.

Endpoint

PUT /{bucket}

Path Parameters

ParameterRequiredDescription
bucket Required Bucket name (3-63 chars, lowercase, no spaces)

Response Codes

  • 200 OK - Bucket created successfully
  • 409 Conflict - Bucket already exists
Request
curl -X PUT http://localhost:9000/my-new-bucket \
    -H "Authorization: Bearer $TOKEN"
Response 200 OK
HTTP/1.1 200 OK
Location: /my-new-bucket
Content-Length: 0
DELETE

Delete Bucket

Deletes an empty bucket. The bucket must be empty before deletion.

Endpoint

DELETE /{bucket}

Path Parameters

ParameterRequiredDescription
bucket Required Name of the bucket to delete
Request
curl -X DELETE http://localhost:9000/my-bucket \
    -H "Authorization: Bearer $TOKEN"
Response 204 No Content
HTTP/1.1 204 No Content
HEAD

Head Bucket

Check if a bucket exists and you have permission to access it.

Endpoint

HEAD /{bucket}
Request
curl -I http://localhost:9000/my-bucket \
    -H "Authorization: Bearer $TOKEN"
Response 200 OK
HTTP/1.1 200 OK
x-amz-bucket-region: us-east-1
Content-Length: 0

Objects

GET

List Objects (v2)

Returns objects in a bucket. Supports pagination and prefix filtering.

Endpoint

GET /{bucket}?list-type=2

Query Parameters

ParameterRequiredDescription
list-typeOptionalUse 2 for ListObjectsV2
prefixOptionalFilter by key prefix
delimiterOptionalGroup keys by delimiter
max-keysOptionalMax results (default: 1000)
continuation-tokenOptionalPagination token
Request
curl -X GET "http://localhost:9000/my-bucket?list-type=2&prefix=docs/&max-keys=10" \
    -H "Authorization: Bearer $TOKEN"
Response 200 OK
<?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

Get Object

Retrieves an object from a bucket. Supports range requests and conditional headers.

Endpoint

GET /{bucket}/{key}

Request Headers

HeaderDescription
RangeByte range (e.g., bytes=0-1023)
If-None-MatchReturn 304 if ETag matches
If-Modified-SinceReturn 304 if not modified

Response Headers

HeaderDescription
ETagObject's MD5 hash
Content-LengthSize in bytes
Content-TypeMIME type
Last-ModifiedLast modification time
x-amz-meta-*Custom metadata
Request
# 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"
Response 200 OK
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

Put Object

Uploads an object to a bucket. Objects are content-addressed using BLAKE3.

Endpoint

PUT /{bucket}/{key}

Request Headers

HeaderDescription
Content-TypeMIME type of the object
Content-LengthSize in bytes
Content-MD5Base64 MD5 for verification
x-amz-meta-*Custom metadata headers
Request
# 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!"
Response 200 OK
HTTP/1.1 200 OK
ETag: "098f6bcd4621d373cade4e832627b4f6"
x-amz-version-id: v1.0
DELETE

Delete Object

Removes an object from a bucket.

Endpoint

DELETE /{bucket}/{key}
Request
curl -X DELETE http://localhost:9000/my-bucket/old-file.txt \
    -H "Authorization: Bearer $TOKEN"
Response 204 No Content
HTTP/1.1 204 No Content
x-amz-delete-marker: true
PUT

Copy Object

Creates a copy of an object. Can copy within or across buckets.

Endpoint

PUT /{bucket}/{key}

Request Headers

HeaderRequiredDescription
x-amz-copy-source Required Source: /bucket/key
Request
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"
Response 200 OK
<?xml version="1.0" encoding="UTF-8"?>
<CopyObjectResult>
    <LastModified>2024-03-20T15:30:00Z</LastModified>
    <ETag>"098f6bcd4621d373cade4e832627b4f6"</ETag>
</CopyObjectResult>
HEAD

Head Object

Retrieves metadata without the object body.

Endpoint

HEAD /{bucket}/{key}
Request
curl -I http://localhost:9000/my-bucket/file.txt \
    -H "Authorization: Bearer $TOKEN"
Response 200 OK
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.

POST

Initiate Multipart Upload

Starts a multipart upload and returns an upload ID.

Endpoint

POST /{bucket}/{key}?uploads
Request
curl -X POST "http://localhost:9000/my-bucket/large-file.zip?uploads" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/zip"
Response 200 OK
<?xml version="1.0" encoding="UTF-8"?>
<InitiateMultipartUploadResult>
    <Bucket>my-bucket</Bucket>
    <Key>large-file.zip</Key>
    <UploadId>VXBsb2FkIElE-abc123xyz</UploadId>
</InitiateMultipartUploadResult>
PUT

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

ParameterRequiredDescription
partNumberRequiredPart number (1-10000)
uploadIdRequiredUpload ID from initiate
Request
# 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
Response 200 OK
HTTP/1.1 200 OK
ETag: "a54357aff0632cce46d942af68356b38"
POST

Complete Multipart Upload

Completes a multipart upload by assembling uploaded parts.

Endpoint

POST /{bucket}/{key}?uploadId={id}
Request
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>'
Response 200 OK
<?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>
DELETE

Abort Multipart Upload

Aborts a multipart upload and frees associated resources.

Endpoint

DELETE /{bucket}/{key}?uploadId={id}
Request
curl -X DELETE "http://localhost:9000/my-bucket/large-file.zip?uploadId=VXBsb2FkIElE-abc123xyz" \
    -H "Authorization: Bearer $TOKEN"
Response 204 No Content
HTTP/1.1 204 No Content

Object Tagging

Add key-value metadata tags to objects for categorization and lifecycle management.

GET

Get Object Tagging

Returns the tag set associated with an object.

Endpoint

GET /{bucket}/{key}?tagging
Request
curl -X GET "http://localhost:9000/my-bucket/file.txt?tagging" \
    -H "Authorization: Bearer $TOKEN"
Response 200 OK
<?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

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
Request
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>'
Response 200 OK
HTTP/1.1 200 OK
Content-Length: 0
DELETE

Delete Object Tagging

Removes all tags from an object.

Endpoint

DELETE /{bucket}/{key}?tagging
Request
curl -X DELETE "http://localhost:9000/my-bucket/file.txt?tagging" \
    -H "Authorization: Bearer $TOKEN"
Response 204 No Content
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.
Encryption Metadata Structure
{
  "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\":\"...\"}"
}
Head Request to Get Metadata
# 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:

Decrypted Private Metadata
{
  "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"
}
Key Obfuscation Modes
# 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 files
  • read_write() - Can read and upload new files
  • full_access() - Can read, write, and delete
Share Token JSON Structure
{
  "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"
}
Sharing Workflow
┌─────────────────────────────────────────────────────────────────┐
│                     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                                │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘