Skip to content

Latest commit

 

History

History
647 lines (478 loc) · 26.7 KB

File metadata and controls

647 lines (478 loc) · 26.7 KB

API Reference

FileMorph provides a REST API for programmatic file conversion and compression. All responses are either a file download (application/octet-stream) or JSON.

Base URL: http://localhost:8000/api/v1


Authentication

FileMorph supports two parallel authentication schemes:

Scheme Header Issued by Use case
API key (Community) X-API-Key: <key> scripts/generate_api_key.py Self-host scripts, automation, CLI tooling
JWT Bearer (Cloud overlay) Authorization: Bearer <token> POST /api/v1/auth/login Browser sessions, multi-user deployments

Either header satisfies the auth requirement on /convert, /compress, and their /batch variants. /health and /formats are public; the auth-flow endpoints (/api/v1/auth/*, /api/v1/keys, /api/v1/billing/*) require a JWT.

API key (Community Edition)

Generate a key:

python scripts/generate_api_key.py
# or via Docker:
docker compose exec filemorph python scripts/generate_api_key.py

Keys are stored as SHA-256 hashes in data/api_keys.json. The plaintext key is shown exactly once at generation time. There is no key-rotation endpoint in the Community Edition — generate a new key and remove the old hash from the JSON file.

JWT Bearer (Cloud overlay)

When DATABASE_URL is configured, the Cloud overlay enables registration / login / refresh:

# Register (returns access + refresh tokens)
curl -X POST http://localhost:8000/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com","password":"correct-horse-battery-staple"}'

# Login on a returning device
curl -X POST http://localhost:8000/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com","password":"correct-horse-battery-staple"}'

# Use the access token
curl http://localhost:8000/api/v1/auth/me \
  -H "Authorization: Bearer <access-token>"

# Refresh expired access tokens (15 min TTL on access, 30 d on refresh)
curl -X POST http://localhost:8000/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token":"<your-refresh-token>"}'

Logged-in users can also generate API keys bound to their account at POST /api/v1/keys; those keys count against the user's tier quota rather than the anonymous tier.

All tokens (access, refresh, password-reset, email-verify) carry the RFC 7519 iss and aud claims — iss=JWT_ISSUER (default filemorph), aud=JWT_AUDIENCE (default filemorph-api) — and every decode path validates them. A token minted by a different FileMorph deployment, or by anything that shares a leaked JWT_SECRET but uses a different audience, is rejected before any business logic runs. Multi-instance operators behind one identity provider should set a distinct JWT_AUDIENCE per instance. Changing either value invalidates every in-flight token on the next request.


Endpoints

Cloud-Edition endpoints (account / billing / keys)

The endpoints in this section only respond when the Cloud overlay is configured (DATABASE_URL set, and where applicable JWT_SECRET, STRIPE_SECRET_KEY). Without those, they return 503 Service Unavailable. All require Authorization: Bearer <jwt> unless noted.

Auth (/api/v1/auth/*)

Method + Path Auth Purpose
POST /api/v1/auth/register none Create account; returns access + refresh tokens. Sends a verification email (fire-and-forget) in the request locale, and stores that locale as preferred_lang.
POST /api/v1/auth/login none Exchange email + password for access (15 min) + refresh (30 d) tokens.
POST /api/v1/auth/refresh none (refresh-token in body) Issue a new access token.
GET /api/v1/auth/me Bearer Return the currently authenticated user (id, email, tier, role, created_at, subscription_status, preferred_lang).
PUT /api/v1/auth/account/language Bearer Set the language for this user's transactional email. Body: {"preferred_lang": "de"|"en"} — an unsupported value is a 422. Returns the updated user object. This is email locale only; the web-UI locale stays URL-prefix driven (no cookie).
POST /api/v1/auth/forgot-password none Issue a single-use password-reset link via email (30 min TTL).
POST /api/v1/auth/reset-password reset-token in body Set a new password and invalidate older sessions via password-hash rotation.
POST /api/v1/auth/verify-email verify-token Mark the user's email as verified.
POST /api/v1/auth/resend-verification Bearer Re-send the verification mail (auth-required to avoid spam).
DELETE /api/v1/auth/account Bearer Self-service account deletion (GDPR Art. 17). Requires re-confirmation: current password, registered email, and the literal string DELETE. Success is 204. Free / never-paid accounts are hard-deleted; an account linked to Stripe is retained in a restricted state — only email, the Stripe customer id, the last tier, and created_at are kept for the 10-year HGB §257 / AO §147 invoice record (permitted by GDPR Art. 17(3)(b)), every other personal field is erased, and the row is hard-deleted at the end of the retention period. Any active Stripe subscription is cancelled first; a Stripe API error returns 500 and leaves the account unchanged. See docs/gdpr-account-deletion-design.md.

API keys (/api/v1/keys)

Method + Path Auth Purpose
POST /api/v1/keys Bearer Create a new API key bound to the authenticated user. Plaintext key is shown exactly once in the response.
GET /api/v1/keys Bearer List the user's keys (id, name, prefix, created, last-used).
DELETE /api/v1/keys/{id} Bearer Revoke a key.

Billing (/api/v1/billing/*)

Method + Path Auth Purpose
POST /api/v1/billing/checkout/{tier} Bearer Start a Stripe Checkout for pro / business. Body MUST include withdrawal_waiver_acknowledged: true (BGB §356 (5) consent — see terms.html § 9). Returns the Stripe Checkout URL; an auth.billing.withdrawal_waiver_recorded audit event is written before the redirect.
POST /api/v1/billing/portal Bearer Return a Stripe Customer Portal URL so the user can manage card / cancel / re-subscribe.
POST /api/v1/billing/webhook Stripe signature Stripe → FileMorph webhook receiver. Handles customer.subscription.{created,updated,deleted} (tier sync from price + status) and invoice.payment_failed (dunning: marks subscription_status=past_due, sends a "payment failed — update your card" email once per retry cycle, keeps the paid tier during Stripe's grace window, and downgrades to Free only on a terminal status). Not exposed in OpenAPI.

For schema details (request bodies, response shapes), open the auto-generated Swagger UI at /docs on the live deployment.

AI operations — PII redaction (Enterprise Edition add-on)

Respond only when AI_OPERATIONS_ENABLED is set; otherwise 503 ai_unavailable. Two-phase. See pii-redaction.md for capability + limits.

Method Endpoint Body Notes
POST /api/v1/ai/redact/detect file, entity_types (optional CSV) Free, open to anonymous/free. Returns JSON {findings:[{entity_type,value,location,confidence}], count, credits_estimate, credits_remaining}.
POST /api/v1/ai/redact/apply file, entity_types (optional), mode (replace|mask|remove) Paid-tier only, credit-metered. Returns the redacted file as a download with headers X-FileMorph-AI-Entities-Redacted, X-FileMorph-AI-Credits-Cost, X-FileMorph-AI-Credits-Remaining.

Supported inputs: UTF-8 text, DOCX, XLSX. Responses are credit-denominated only — no model id, token count, or euro cost. Error codes (X-FileMorph-Error-Code): ai_unavailable (503), ai_plan_required (403), ai_credits_exhausted (402), unsupported_format (415, incl. PDF by design), input_too_large (413), unknown_entity_type (400), document_unreadable (400), redaction_verification_failed (500, fail-closed — no file returned), output_cap_exceeded (413).


POST /api/v1/convert

Convert a file from one format to another.

Authentication: Required (X-API-Key header)

Request: multipart/form-data

Field Type Required Description
file file Yes The file to convert
target_format string Yes Target format extension, e.g. jpg, pdf, mp3
quality integer No Quality 1–100 (default: 85). Applies to lossy formats (JPEG, WebP, AVIF, video)

Response: 200 OK — the converted file as a download

Example — HEIC to JPG

curl -X POST http://localhost:8000/api/v1/convert \
  -H "X-API-Key: YOUR_KEY" \
  -F "file=@photo.heic" \
  -F "target_format=jpg" \
  -F "quality=90" \
  --output photo.jpg

Example — DOCX to PDF

curl -X POST http://localhost:8000/api/v1/convert \
  -H "X-API-Key: YOUR_KEY" \
  -F "file=@document.docx" \
  -F "target_format=pdf" \
  --output document.pdf

Example — Python (requests)

import requests

key = "YOUR_KEY"
with open("photo.heic", "rb") as f:
    response = requests.post(
        "http://localhost:8000/api/v1/convert",
        headers={"X-API-Key": key},
        files={"file": ("photo.heic", f, "image/heic")},
        data={"target_format": "jpg", "quality": 85},
    )

with open("photo.jpg", "wb") as out:
    out.write(response.content)

Example — JavaScript (fetch)

const formData = new FormData();
formData.append("file", fileInput.files[0]);
formData.append("target_format", "jpg");
formData.append("quality", "85");

const response = await fetch("http://localhost:8000/api/v1/convert", {
  method: "POST",
  headers: { "X-API-Key": "YOUR_KEY" },
  body: formData,
});

const blob = await response.blob();
const url = URL.createObjectURL(blob);
// use url for download link

POST /api/v1/compress

Reduce a file's size by re-encoding at a lower quality, keeping the same format.

Authentication: Required (X-API-Key header)

Request: multipart/form-data

Field Type Required Description
file file Yes The file to compress
quality integer No Quality 1 (smallest) – 100 (best). Defaults to 85. Mutually exclusive with target_size_kb
target_size_kb integer No Target output size in KB. Activates binary-search-on-quality (JPEG/WebP/AVIF only). Mutually exclusive with quality

Supported formats: JPG, JPEG, PNG, WebP, AVIF, TIFF · MP4, MOV, AVI, MKV, WebM

target_size_kb is JPEG/WebP/AVIF only — PNG/TIFF are lossless and quality does not control size meaningfully. Sending target_size_kb with a PNG returns 415. AVIF/AV1 encode is more CPU-intensive than JPEG/WebP, and target-size runs several encode passes.

Response: 200 OK — the compressed file as a download (same format, _compressed suffix in filename).

When target_size_kb is set, the response also carries:

Header Description
X-FileMorph-Achieved-Bytes Actual output size in bytes
X-FileMorph-Final-Quality Quality value the search settled on (1–100)

Tolerance is ±3 % of the requested target. If even quality 1 exceeds the target, the smallest possible output is returned anyway and the headers reveal the actual size.

Example — Compress a JPG to 70% quality

curl -X POST http://localhost:8000/api/v1/compress \
  -H "X-API-Key: YOUR_KEY" \
  -F "file=@large_photo.jpg" \
  -F "quality=70" \
  --output smaller_photo.jpg

Example — Compress a JPG to a 500 KB target

curl -X POST http://localhost:8000/api/v1/compress \
  -H "X-API-Key: YOUR_KEY" \
  -F "file=@large_photo.jpg" \
  -F "target_size_kb=500" \
  -D headers.txt \
  --output capped_photo.jpg

# headers.txt now contains X-FileMorph-Achieved-Bytes / X-FileMorph-Final-Quality

Example — Compress a video

curl -X POST http://localhost:8000/api/v1/compress \
  -H "X-API-Key: YOUR_KEY" \
  -F "file=@recording.mp4" \
  -F "quality=60" \
  --output recording_compressed.mp4

Quality guide for images

Quality Typical size reduction Visual difference
90 ~20% Nearly invisible
80 ~40% Very subtle
70 ~55% Slightly noticeable on close inspection
60 ~65% Noticeable, acceptable for web thumbnails
50 ~70% Clearly visible, good for previews

POST /api/v1/pdf/extract

Write a new PDF containing only a selected page range — a structural morph, not a format conversion. Text, fonts and vector content are copied intact.

Authentication: Required (X-API-Key or Authorization: Bearer)

Request: multipart/form-data

Field Type Required Description
file file Yes The source PDF (must be a .pdf)
pages string Yes 1-based page selection — comma-separated singletons and a-b ranges, e.g. 1-3,5. Sorted + de-duplicated.

Response: 200 OK (application/pdf) — the extracted pages as a download (_pages.pdf suffix). An empty / reversed (5-3) / out-of-range / non-numeric selection is a 400; a non-PDF input is a 422. A selection may resolve to at most 10 000 pages.

curl -X POST http://localhost:8000/api/v1/pdf/extract \
  -H "X-API-Key: YOUR_KEY" \
  -F "file=@report.pdf" \
  -F "pages=1-3,5" \
  --output report_pages.pdf

POST /api/v1/pdf/split

Split a PDF into one single-page PDF per page, bundled as a ZIP.

Authentication: Required (X-API-Key or Authorization: Bearer)

Request: multipart/form-data

Field Type Required Description
file file Yes The source PDF (must be a .pdf)

Response: 200 OK (application/zip) — one entry per page (page_001.pdf, page_002.pdf, … zero-padded to the page count's width, so entries sort correctly), _pages.zip suffix. A non-PDF input is a 422; a document over 10 000 pages is a 400 (rejected before any work). The assembled ZIP must also fit the tier output cap (413 otherwise).

curl -X POST http://localhost:8000/api/v1/pdf/split \
  -H "X-API-Key: YOUR_KEY" \
  -F "file=@report.pdf" \
  --output report_pages.zip

POST /api/v1/pdf/compress

Shrink a PDF toward a byte budget by recompressing its embedded raster images. Page count and every glyph are preserved; text/fonts/vector content are left intact. Honest by design — a PDF with no recompressible images comes back valid and unchanged-in-content (see the headers below), never a fake compression claim.

Authentication: Required (X-API-Key or Authorization: Bearer)

Request: multipart/form-data

Field Type Required Description
file file Yes The source PDF (must be a .pdf)
target_kb integer Yes Target output size in KB (> 0). The embedded images are recompressed toward this budget via binary search on a global JPEG quality.

Response: 200 OK (application/pdf) — the compressed PDF as a download (_compressed.pdf suffix), plus:

Header Description
X-FileMorph-Achieved-Bytes Actual output size in bytes
X-FileMorph-Converged true if the output reached the target within tolerance, else false
X-FileMorph-Recompressible-Images How many embedded images the engine could recompress (0 ⇒ nothing to shrink)

A target_kb above the tier output cap is a 413 (rejected before any work); a value beyond a 2 GB sanity ceiling, or a non-PDF / corrupt input, is a 422 / 400. A text/vector-only PDF returns 200 with X-FileMorph-Converged: false and X-FileMorph-Recompressible-Images: 0.

curl -X POST http://localhost:8000/api/v1/pdf/compress \
  -H "X-API-Key: YOUR_KEY" \
  -F "file=@scan.pdf" \
  -F "target_kb=500" \
  -D headers.txt \
  --output scan_compressed.pdf

# headers.txt now carries X-FileMorph-Achieved-Bytes / -Converged /
# -Recompressible-Images

POST /api/v1/convert/batch

Convert several files in one request. Returns a ZIP archive with all converted outputs.

Authentication: Required (X-API-Key or Authorization: Bearer)

Request: multipart/form-data

Field Type Required Description
files files (≥1) Yes One or more files to convert
target_formats string[] Yes Target format per file, repeated once per file in the same order (length must match files, otherwise 422)
quality integer No Quality 1–100 (default 85). Applied uniformly.

Response: 200 OK (application/zip) — archive with one entry per successful conversion. If at least one file fails, a manifest.json is added at archive root listing per-file results (success ZIP-only is preferred for all-success runs to keep the output clean).

A run with every file failing returns 422 Unprocessable Content with a JSON body listing per-file errors.

curl -X POST http://localhost:8000/api/v1/convert/batch \
  -H "X-API-Key: YOUR_KEY" \
  -F "files=@a.heic" -F "files=@b.png" -F "files=@c.gif" \
  -F "target_formats=jpg" -F "target_formats=jpg" -F "target_formats=jpg" \
  --output batch.zip

POST /api/v1/compress/batch

Compress several files in one request. Same response shape as /convert/batch.

Authentication: Required

Request: multipart/form-data

Field Type Required Description
files files (≥1) Yes One or more files to compress
quality integer No Quality 1–100 (default 85). Mutually exclusive with target_size_kb.
target_size_kb integer No Per-file target size. Mutually exclusive with quality.
curl -X POST http://localhost:8000/api/v1/compress/batch \
  -H "X-API-Key: YOUR_KEY" \
  -F "files=@photo1.jpg" -F "files=@photo2.jpg" \
  -F "quality=70" \
  --output batch.zip

GET /api/v1/formats

Returns all supported conversion and compression formats.

Authentication: Not required

Response: 200 OK — JSON

{
  "conversions": {
    "jpg": ["png", "webp", "avif", "bmp", "tiff", "gif"],
    "heic": ["jpg", "png", "webp", "avif"],
    "docx": ["pdf", "txt"],
    "txt": ["pdf"],
    "csv": ["xlsx", "json"],
    "mp4": ["avi", "mov", "mkv", "webm"],
    "mp3": ["wav", "flac", "ogg", "m4a"]
  },
  "compression": {
    "image": ["jpg", "jpeg", "png", "webp", "avif", "tiff"],
    "video": ["mp4", "avi", "mov", "mkv", "webm"]
  }
}

Use this endpoint to populate format selection dropdowns in your application.


GET /api/v1/health

Health check for monitoring and load balancer probes.

Authentication: Not required

Response: 200 OK — JSON

{"status": "ok"}

/api/v1/health is the unauthenticated liveness probe — it stays deliberately minimal (no version or codec flags) so a public hit does not disclose deployment internals (pentest finding PT-011). For operational state (database / temp-dir reachability, ffmpeg-on-PATH), use GET /api/v1/ready.


POST /api/v1/contact

Public contact form, linked from the German Impressum as the second, fast-direct contact channel required by DDG §5 (ECJ C-298/07). Anonymous; works on the Community edition too — the message is emailed to the operator (recipient resolved from CONTACT_FORM_RECIPIENT_EMAIL → SMTP_REPLY_TO → SMTP_FROM_EMAIL), with Reply-To set to the submitter. The message is not persisted server-side — only an audit event with a hashed email + the visitor's locale is recorded.

Authentication: Not required · Rate limit: 5 / hour per IP

Request body (JSON): email (required), message (required, 20–5000 chars), name (optional, ≤120), subject (optional, ≤160). Anti-spam: a hidden honeypot field plus the rate limit — automated-looking submissions receive a normal 200 and are silently dropped.

Responses: 200 {"detail": "Message sent."} on success; 422 on validation errors; 429 when rate-limited; 502 if delivery fails (the UI then offers the direct mailto: fallback).


Response Headers

Every successful conversion / compression carries integrity and classification metadata in response headers. CORS-enabled deployments expose these to browser clients (see expose_headers in app/main.py).

Header Value Set on
X-Output-SHA256 Hex-encoded SHA-256 of the response body every single-file /convert and /compress (not the batch ZIPs)
X-Data-Classification One of public, internal, confidential, restricted every response — echoes the request header value, defaults to internal when absent (NEU-C.3 / BSI-style taxonomy)
X-FileMorph-Achieved-Bytes Actual output size in bytes on /compress calls with target_size_kb, and on /pdf/compress
X-FileMorph-Final-Quality Quality value the binary search settled on (1–100) only on /compress calls with target_size_kb
Retry-After Seconds the client should wait before retrying on 503 Service Unavailable (global concurrency cap) and on every 429 except the rate limiter's (slowapi, see Rate Limiting) — e.g. the per-tier concurrency cap and the monthly call quota

The X-Data-Classification value is also written to the audit-log entry for the request, so a downstream auditor can answer "what classification of data was processed in this call" from the database alone (see app/core/audit.py).


Error Responses

Errors return JSON with a detail field:

{
  "detail": "Conversion from 'jpg' to 'docx' is not supported."
}

Two exceptions: a 429 from the rate limiter (slowapi) reads {"error": "Rate limit exceeded: 10 per 1 minute"} — with the limit of the route you called — and has no Retry-After header; a batch in which every file failed returns 422 with {"summary": …, "files": […]}.

HTTP Status Meaning
400 Bad Request Missing or malformed request data (e.g. filename without extension), or file content that has to be fixed first — e.g. a Markdown, CSV or JSON file that isn't UTF-8 (X-FileMorph-Error-Code: invalid_input; detail names the fix)
401 Unauthorized Missing or invalid X-API-Key / Authorization: Bearer
403 Forbidden Authenticated but role/tier doesn't permit the action (e.g. non-admin hitting /cockpit/*)
413 Content Too Large Request exceeds MAX_UPLOAD_SIZE_MB (default: 100 MB), a file exceeds your tier's size cap, or the output exceeds your tier's output cap
415 Unsupported Media Type target_size_kb set on a lossless format (PNG/TIFF), or otherwise incompatible request shape
422 Unprocessable Content Unsupported format combination, missing form field, target_formats count ≠ files count, or every file in a batch failed
429 Too Many Requests Rate limit exceeded (see Rate Limiting section below), per-tier concurrency cap reached, or monthly call quota used up
500 Internal Server Error Conversion failed (e.g. corrupt file, missing binary)
503 Service Unavailable Global concurrency cap reached (MAX_GLOBAL_CONCURRENCY). Response carries Retry-After.

Rate Limiting

Per-route limits (per IP address):

Endpoint Limit
POST /api/v1/convert 10 / minute
POST /api/v1/convert/batch 3 / minute
POST /api/v1/compress 10 / minute
POST /api/v1/compress/batch 3 / minute
GET /api/v1/health, GET /api/v1/ready 30 / minute
GET /api/v1/formats 120 / minute
Auth endpoints (/api/v1/auth/*) 3–5 / minute
Billing endpoints (/api/v1/billing/*) 5 / minute
POST /api/v1/contact 5 / hour
Default (other routes) 60 / minute

When exceeded, the response is 429 Too Many Requests. For higher limits, self-host your own instance and adjust the decorators in app/api/routes/*.py (slowapi @limiter.limit("…/minute")).

Monthly call quota (per user)

Authenticated users are also limited per calendar month, independently of the per-IP rate limits above:

Tier Monthly API calls
Anonymous n/a (per-IP rate-limit only)
Free 1,000
Pro 25,000
Business 200,000
Enterprise unlimited

The gate counts every successful POST /api/v1/convert, /convert/batch, /compress, /compress/batch, /pdf/extract, /pdf/split, and /pdf/compress as one call. A batch with 25 files counts as 1 call (matching the pricing-page wording "API calls per month"). Failed conversions do not count toward the quota.

When the limit is reached, the response is 429 Too Many Requests with a Retry-After header in seconds pointing at the start of the next calendar month, and a body explaining the limit:

{
  "detail": "Monthly API call limit reached (25000 per month for tier 'pro'). Quota resets 2026-06-01T00:00:00+00:00. Upgrade your plan or wait until the reset to continue."
}

The quota window is calendar-month UTC — the counter resets at 00:00 UTC on the 1st of every month. The pricing page advertises identical figures; this gate is the runtime side of that promise.


Swagger / OpenAPI

FileMorph auto-generates interactive API documentation:

  • Swagger UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc
  • OpenAPI JSON: http://localhost:8000/openapi.json

The Swagger UI lets you test all endpoints directly in the browser.


Integration Examples

PHP

$ch = curl_init('http://localhost:8000/api/v1/convert');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['X-API-Key: YOUR_KEY'],
    CURLOPT_POSTFIELDS => [
        'file' => new CURLFile('/path/to/photo.heic', 'image/heic', 'photo.heic'),
        'target_format' => 'jpg',
        'quality' => '85',
    ],
]);
$result = curl_exec($ch);
file_put_contents('/path/to/photo.jpg', $result);

Node.js

const FormData = require('form-data');
const fs = require('fs');
const axios = require('axios');

const form = new FormData();
form.append('file', fs.createReadStream('document.docx'));
form.append('target_format', 'pdf');

const response = await axios.post(
  'http://localhost:8000/api/v1/convert',
  form,
  {
    headers: { ...form.getHeaders(), 'X-API-Key': 'YOUR_KEY' },
    responseType: 'arraybuffer',
  }
);
fs.writeFileSync('document.pdf', response.data);

C# / .NET

using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "YOUR_KEY");

using var form = new MultipartFormDataContent();
form.Add(new StreamContent(File.OpenRead("photo.heic")), "file", "photo.heic");
form.Add(new StringContent("jpg"), "target_format");

var response = await client.PostAsync(
    "http://localhost:8000/api/v1/convert", form);
await File.WriteAllBytesAsync("photo.jpg", await response.Content.ReadAsByteArrayAsync());