API Documentation

Integrate ImageHost into your applications with our REST API. All image data is end-to-end encrypted.

Base URL

http://img.aither.cc/api/v1

All API endpoints are prefixed with /api/v1

Encryption Options

ImageHost uses AES-256-GCM encryption. How and where encryption happens depends on which upload method you use.

🚀 Which Method Should I Use?

EASY

Web Dashboard

Just use the Upload page. Encryption happens in your browser - the server never sees your images.

✓ True end-to-end encryption (client-side)

SIMPLE

Screenshot Tools (ShareX, Flameshot)

Use /api/v1/tools/upload - send your image and the server encrypts it before storage.

curl -X POST -H "X-API-Key: YOUR_KEY" -F "file=@image.png" http://img.aither.cc/api/v1/tools/upload

⚠️ Server-side encryption: Your image travels to the server unencrypted (over HTTPS), then the server encrypts it. This is convenient but the server briefly sees your plaintext image. For maximum privacy, use the web dashboard or implement client-side encryption.

ADVANCED

Custom Client Development

Building your own app? Implement client-side encryption using the technical details below.

✓ True end-to-end encryption (client-side)

💡 Summary: For true zero-knowledge privacy, use the web dashboard or build a custom client with client-side encryption. The tools endpoint is convenient but involves server-side encryption.

Show Technical Encryption Details (for developers)

Key Hierarchy

Password
    │
    └─▶ Argon2id ─▶ Master Key (256-bit)
                        │
                        └─▶ Encrypts per-image keys
                                    │
                                    └─▶ Image Key (256-bit, random)
                                                │
                                                └─▶ Encrypts image data
  • Master Key: Derived from your password using Argon2id. Stored encrypted on server.
  • Image Key: Random 256-bit key generated for each image. Provides key isolation.
  • Share URLs: Use URL fragments (#key) which are never sent to the server.

Encrypted File Format

All encrypted blobs use the following binary format:

┌──────────────────────────────────────────────────────────────┐
│ Offset │ Size     │ Field                                   │
├────────┼──────────┼─────────────────────────────────────────┤
│ 0      │ 4 bytes  │ Magic: "IHE1" (0x49 0x48 0x45 0x31)      │
│ 4      │ 1 byte   │ Version: 0x01                            │
│ 5      │ 12 bytes │ IV (Initialization Vector)              │
│ 17     │ N bytes  │ Ciphertext (encrypted data)             │
│ 17+N   │ 16 bytes │ GCM Authentication Tag                  │
└────────┴──────────┴─────────────────────────────────────────┘

Note: The GCM auth tag is automatically appended by the encryption. Total overhead is 33 bytes (5 header + 12 IV + 16 tag).

JavaScript Example (Web Crypto API)

// Constants
const MAGIC = new Uint8Array([0x49, 0x48, 0x45, 0x31]); // "IHE1"
const VERSION = 0x01;
const IV_LENGTH = 12;

// Generate a random 256-bit key
async function generateKey() {
    return await crypto.subtle.generateKey(
        { name: 'AES-GCM', length: 256 },
        true,  // extractable
        ['encrypt', 'decrypt']
    );
}

// Encrypt image data
async function encryptImage(imageData, key) {
    // Generate random IV
    const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));

    // Encrypt with AES-256-GCM
    const ciphertext = await crypto.subtle.encrypt(
        { name: 'AES-GCM', iv: iv, tagLength: 128 },
        key,
        imageData
    );

    // Build output: magic + version + iv + ciphertext
    const header = new Uint8Array([...MAGIC, VERSION]);
    const result = new Uint8Array(
        header.length + iv.length + ciphertext.byteLength
    );
    result.set(header, 0);
    result.set(iv, header.length);
    result.set(new Uint8Array(ciphertext), header.length + iv.length);

    return result;
}

// Export key as URL-safe base64 (for sharing)
async function exportKeyForSharing(key) {
    const raw = await crypto.subtle.exportKey('raw', key);
    const base64 = btoa(String.fromCharCode(...new Uint8Array(raw)));
    return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

// Usage
const imageFile = /* File from input */;
const imageData = await imageFile.arrayBuffer();
const key = await generateKey();
const encrypted = await encryptImage(imageData, key);
const keyString = await exportKeyForSharing(key);

// Upload encrypted blob to API
// Share URL: https://example.com/v/abc123#${keyString}

Python Example (cryptography library)

import os
import base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

# Constants
MAGIC = b'IHE1'
VERSION = b'\x01'
IV_LENGTH = 12
KEY_LENGTH = 32  # 256 bits

def generate_key() -> bytes:
    """Generate a random 256-bit key."""
    return os.urandom(KEY_LENGTH)

def encrypt_image(image_data: bytes, key: bytes) -> bytes:
    """Encrypt image data using AES-256-GCM."""
    # Generate random IV
    iv = os.urandom(IV_LENGTH)

    # Encrypt
    aesgcm = AESGCM(key)
    ciphertext = aesgcm.encrypt(iv, image_data, None)

    # Build output: magic + version + iv + ciphertext
    return MAGIC + VERSION + iv + ciphertext

def key_to_url_safe(key: bytes) -> str:
    """Convert key to URL-safe base64 for sharing."""
    return base64.urlsafe_b64encode(key).rstrip(b'=').decode()

# Usage
with open('image.png', 'rb') as f:
    image_data = f.read()

key = generate_key()
encrypted = encrypt_image(image_data, key)
key_string = key_to_url_safe(key)

# Upload 'encrypted' to the API
# Share URL: https://example.com/v/abc123#{key_string}

Decryption

To decrypt, parse the binary format and use the IV with the key:

# Python decryption example
def decrypt_image(encrypted_data: bytes, key: bytes) -> bytes:
    # Verify magic header
    if encrypted_data[:4] != MAGIC:
        raise ValueError("Invalid magic header")

    # Verify version
    if encrypted_data[4:5] != VERSION:
        raise ValueError("Unsupported version")

    # Extract IV and ciphertext
    iv = encrypted_data[5:5 + IV_LENGTH]
    ciphertext = encrypted_data[5 + IV_LENGTH:]

    # Decrypt
    aesgcm = AESGCM(key)
    return aesgcm.decrypt(iv, ciphertext, None)

The encrypted_image_key Field

When uploading via the API, you must provide the per-image key encrypted with your master key. This allows you to decrypt your own images later from your gallery.

# The encrypted_image_key is the per-image key encrypted with your master key
# using the same AES-256-GCM format

image_key = generate_key()  # Random key for this image
encrypted_image_key = encrypt_with_master_key(image_key, master_key)

# The server stores encrypted_image_key but cannot decrypt it
# Only you can recover image_key using your master_key

Note: This client-side encryption flow is used by the web dashboard and custom clients. The /api/v1/tools/upload endpoint uses server-side encryption instead (see Encryption Options above for details).

URL Types & Security Trade-offs

ImageHost provides multiple URL formats with different security and usability characteristics:

URL Type Format Security Use Case
Viewer /v/{code}#key 🔒 E2EE - Key never sent to server Sharing links to people, maximum privacy
Encrypted /e/{code} 🔒 E2EE - Raw encrypted blob API access, client-side decryption
Embed /i/{code}?key=... ⚠️ Server decrypts Forum <img> tags, BBCode
Thumbnail /t/{code} 🔒 E2EE - Encrypted thumbnail Preview grids, galleries

Embed URLs break end-to-end encryption

When using /i/{code}?key=..., the decryption key is sent to the server as a query parameter. The server decrypts the image and serves it as a standard image file. This is necessary for <img src="..."> tags to work in forums and HTML pages, but means the server temporarily sees your image data.

How URL Fragments Preserve Privacy

The # fragment in viewer URLs is special: per HTTP specification, browsers never send fragments to servers. The decryption key stays entirely in the browser.

https://http://img.aither.cc/v/abc123#Wj3kL9mN...
                           │          └─ URL-safe base64 key (never sent to server)
                           └─ Short code (server uses this)

Authentication

ImageHost uses JWT tokens for authentication. You can authenticate using either:

  • Cookie-based: Tokens are automatically stored in HTTP-only cookies after login
  • API Key: Generate an API key in Settings for programmatic access
POST /api/v1/auth/login

Authenticate with email and password.

Request Body (form-urlencoded)

username=user@example.com
password=yourpassword

Response

{
  "id": "uuid",
  "username": "user",
  "email": "user@example.com",
  "access_token": "jwt...",
  "token_type": "bearer"
}
POST /api/v1/auth/refresh

Get a new access token using your refresh token (stored in cookies).

POST /api/v1/auth/logout

Logout and invalidate your refresh token.

GET /api/v1/auth/me

Get the current authenticated user's information.

Response

{
  "id": "uuid",
  "username": "user",
  "email": "user@example.com",
  "storage_used_mb": 150,
  "storage_quota_mb": 5000,
  "created_at": "2024-01-01T00:00:00Z"
}

Images

Upload, retrieve, and manage your encrypted images. All images are encrypted client-side before upload.

POST /api/v1/images/upload

Upload an encrypted image.

Request Body (multipart/form-data)

encrypted_blob: (binary)       # Encrypted image data
encrypted_thumbnail: (binary)  # Encrypted thumbnail
encrypted_filename: (string)   # Encrypted original filename
encrypted_metadata: (string)   # Encrypted metadata JSON
encrypted_image_key: (string)  # Image key encrypted with master key
mime_type: (string)            # Original MIME type
is_public: (boolean)           # Optional, default true
expires_at: (datetime)         # Optional expiration

Response

{
  "id": "uuid",
  "short_code": "abc123",
  "view_url": "/v/abc123",
  "encrypted_url": "/e/abc123",
  "embed_url": "/i/abc123?key=...",
  "created_at": "2024-01-01T00:00:00Z"
}
GET /api/v1/images

List your images with pagination.

Query Parameters

page: (int)      # Page number, default 1
per_page: (int)  # Items per page, default 20, max 100
sort_by: (str)   # created_at, file_size, view_count
order: (str)     # asc or desc
GET /api/v1/images/{short_code}

Get image metadata by short code.

PATCH /api/v1/images/{image_id}

Update image settings.

{
  "is_public": true,
  "expires_at": "2024-12-31T23:59:59Z"
}
DELETE /api/v1/images/{image_id}

Delete an image (soft delete, can be restored within 30 days).

Albums

Organize your images into encrypted albums.

POST /api/v1/albums

Create a new album.

{
  "encrypted_name": "base64...",
  "encrypted_description": "base64...",
  "is_public": false
}
GET /api/v1/albums

List your albums.

GET /api/v1/albums/{album_id}

Get album details and images.

POST /api/v1/albums/{album_id}/images

Add images to an album.

{
  "image_ids": ["uuid1", "uuid2"]
}
DELETE /api/v1/albums/{album_id}

Delete an album (images are not deleted).

API Keys

Generate API keys for programmatic access. Include the key in the X-API-Key header.

curl -H "X-API-Key: your-api-key" \
     http://img.aither.cc/api/v1/images
POST /api/v1/keys

Create a new API key.

{
  "name": "My App",
  "permissions": ["read", "write"],
  "expires_in_days": 365
}
GET /api/v1/keys

List your API keys (key values are not shown).

DELETE /api/v1/keys/{key_id}

Revoke an API key.

Tools Integration

Integrate with screenshot tools like ShareX, Flameshot, and more.

GET /api/v1/tools/sharex-config

Download ShareX configuration file.

Requires API key authentication.

GET /api/v1/tools/flameshot-script

Download Flameshot upload script for Linux.

POST /api/v1/tools/upload

The easiest way to upload! Just send your image - we handle encryption automatically.

Request (multipart/form-data)

file: (binary)           # Your image file (JPEG, PNG, GIF, WebP)
is_public: (boolean)     # Optional, default true
link_type: (string)      # Optional: "viewer" (default), "embed", or "direct"

Link Type Options

viewer Returns /v/code#key - Private URL, key never sent to server (default)
embed Returns /i/code?key=xxx - For <img> tags in forums/Discord
direct Returns /e/code - Raw encrypted blob (decrypt yourself)

Examples

# Default (viewer URL - most private)
curl -X POST -H "X-API-Key: your-key" -F "file=@screenshot.png" \
  http://img.aither.cc/api/v1/tools/upload

# Get embed URL (for forum posts, Discord)
curl -X POST -H "X-API-Key: your-key" -F "file=@screenshot.png" -F "link_type=embed" \
  http://img.aither.cc/api/v1/tools/upload

Response

{
  "success": true,
  "id": "uuid",
  "short_code": "abc123",
  "url": "http://img.aither.cc/v/abc123#key",        // Primary URL (based on link_type)
  "share_url": "http://img.aither.cc/v/abc123#key",   // Always viewer URL
  "embed_url": "http://img.aither.cc/i/abc123?key=xxx", // Always embed URL
  "encrypted_url": "http://img.aither.cc/e/abc123",   // Raw encrypted blob
  "thumbnail_url": "http://img.aither.cc/t/abc123#key",
  "delete_url": "http://img.aither.cc/api/v1/images/uuid",
  "decryption_key": "URL-safe-base64-key",
  "link_type": "viewer",                        // Confirms which type was used
  "note": "Viewer URL returned. The decryption key..."
}

✅ Images are encrypted at rest using AES-256-GCM. The url field contains your primary link based on link_type.

⚠️ Security note: The server briefly sees your image during encryption. For maximum security (true E2EE), use client-side encryption.

User Profile

GET /api/v1/users/me

Get your profile information.

PATCH /api/v1/users/me

Update your profile.

{
  "username": "newusername"
}
POST /api/v1/users/me/password

Change your password.

{
  "current_password": "...",
  "new_password": "...",
  "new_encrypted_master_key": "...",
  "new_master_key_salt": "..."
}

Password changes require re-encrypting your master key with the new password.

GET /api/v1/users/me/storage

Get your storage usage statistics.

GET /api/v1/users/me/stats

Get detailed statistics including image count, album count, and storage breakdown.

GET /api/v1/users/me/sessions

List all active login sessions.

Response

{
  "sessions": [
    {
      "session_id": "abc123...",
      "ip_address": "192.168.1.1",
      "device_info": "Chrome on Windows",
      "created_at": "2024-01-01T00:00:00Z",
      "expires_at": "2024-01-08T00:00:00Z",
      "is_current": true
    }
  ],
  "total": 1
}
DELETE /api/v1/users/me/sessions/{session_id}

Terminate a specific session (log out that device).

DELETE /api/v1/users/me/sessions

Terminate all sessions except the current one (log out all other devices).

PUT /api/v1/users/me/keys

Update your encrypted master key (used during password change or key rotation).

{
  "encrypted_master_key": "base64...",
  "master_key_salt": "base64..."
}
DELETE /api/v1/users/me

Permanently delete your account and all images.

⚠️ Warning: This action is irreversible. All your images will be destroyed.

Public URLs & Embedding

ImageHost provides different URL formats for different use cases. Understanding when to use each is important for both privacy and functionality.

PRIVATE /v/{short_code}#key

Viewer Page - Interactive viewer with download option.

The key is in the URL fragment (#) which is never sent to the server. Best for sharing via messaging apps where you control access.

EMBEDDABLE /i/{short_code}?key=xxx

Embed URL - Direct image for <img> tags.

The key is in the query string (?key=) which is sent to the server. Use for forums, Discord, or anywhere you need <img src="..."> to work.

<img src="http://img.aither.cc/i/abc123?key=YOUR_KEY" alt="Image">
RAW /e/{short_code}

Encrypted Blob - Raw encrypted data for custom clients.

Returns the encrypted image data. You must decrypt it yourself using the decryption key. For advanced integrations building their own decryption.

THUMBNAIL /t/{short_code}

Encrypted Thumbnail - Smaller preview image (encrypted).

Returns the encrypted thumbnail. Use the same key as the full image to decrypt.

Privacy Comparison

Viewer (#key) ✓ Server never sees key
Embed (?key=) ⚠ Server sees key to decrypt for embedding
Encrypted ✓ Server never sees key (you decrypt client-side)

Error Responses

All errors follow a consistent format:

{
  "detail": "Error message here"
}

Common Status Codes

200 Success
201 Created
400 Bad Request - Invalid input
401 Unauthorized - Invalid or missing authentication
403 Forbidden - Insufficient permissions
404 Not Found - Resource doesn't exist
429 Too Many Requests - Rate limited
500 Internal Server Error

Rate Limits

API requests are rate limited to protect the service. Current limits:

General API 100 requests/minute
Image Upload 20 uploads/minute
Login Attempts 5 attempts/15 minutes
Registration 3 registrations/hour

Rate limit headers are included in responses: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset