{"openapi":"3.1.0","info":{"title":"FileMorph","description":"Convert and compress files between formats via REST API or Web UI.\n\n**Authentication**: optional on the file endpoints — without credentials a request runs on the anonymous tier. An `X-API-Key` that is sent must be valid (otherwise 401; 429 once an IP has sent 30 rejected keys in a minute). Keys come from the dashboard (Cloud Edition) or from `python scripts/generate_api_key.py` (self-hosted). The account endpoints take `Authorization: Bearer <token>` from `/api/v1/auth/login`.","version":"1.5.0"},"paths":{"/api/v1/health":{"get":{"tags":["System"],"summary":"Health Check","description":"Liveness probe — the process is up and the framework is responding.\nDeliberately cheap: no DB or disk I/O, no dependency checks. Use /ready\nfor the full dependency check.\n\nReturns only ``{\"status\": \"ok\"}``. The app version and codec\navailability are intentionally not exposed on this unauthenticated\nendpoint (pentest finding PT-011); operators read the running version\nfrom the deployed image tag / release.","operationId":"health_check_api_v1_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/api/v1/ready":{"get":{"tags":["System"],"summary":"Readiness Check","description":"Readiness probe — the app is ready to serve real traffic.\n\nFails (HTTP 503) if:\n  * a configured database is unreachable, or\n  * the tempdir is not writable (no conversions possible without it).\n\nWhen no DB is configured (Community Edition), the DB check is reported\nas ``skipped`` and does not fail readiness.","operationId":"readiness_check_api_v1_ready_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/formats":{"get":{"tags":["Formats"],"summary":"List Formats","operationId":"list_formats_api_v1_formats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FormatsResponse"}}}}}}},"/api/v1/convert":{"post":{"tags":["Convert"],"summary":"Convert File","operationId":"convert_file_api_v1_convert_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_convert_file_api_v1_convert_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/convert/batch":{"post":{"tags":["Convert"],"summary":"Convert Batch","operationId":"convert_batch_api_v1_convert_batch_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_convert_batch_api_v1_convert_batch_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/compress":{"post":{"tags":["Compress"],"summary":"Compress File","operationId":"compress_file_api_v1_compress_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_compress_file_api_v1_compress_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/compress/batch":{"post":{"tags":["Compress"],"summary":"Compress Batch","operationId":"compress_batch_api_v1_compress_batch_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_compress_batch_api_v1_compress_batch_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/pdf/extract":{"post":{"tags":["Convert"],"summary":"Pdf Extract","operationId":"pdf_extract_api_v1_pdf_extract_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_pdf_extract_api_v1_pdf_extract_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/pdf/split":{"post":{"tags":["Convert"],"summary":"Pdf Split","operationId":"pdf_split_api_v1_pdf_split_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_pdf_split_api_v1_pdf_split_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/pdf/compress":{"post":{"tags":["Compress"],"summary":"Pdf Compress","operationId":"pdf_compress_api_v1_pdf_compress_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_pdf_compress_api_v1_pdf_compress_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/pdf/insert":{"post":{"tags":["Convert"],"summary":"Pdf Insert","operationId":"pdf_insert_api_v1_pdf_insert_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_pdf_insert_api_v1_pdf_insert_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ai/redact/detect":{"post":{"tags":["AI"],"summary":"Detect PII in a file (free findings preview)","description":"Phase 1 of redaction: scan UTF-8 text, DOCX or XLSX for structured PII (IBAN, email, phone, IPv4, credit-card) and return the findings to review. Free, no credit charged. Returns 503 when AI operations are not enabled on this deployment.","operationId":"redact_detect_api_v1_ai_redact_detect_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_redact_detect_api_v1_ai_redact_detect_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ai/redact/apply":{"post":{"tags":["AI"],"summary":"Produce the redacted file (paid, credit-metered)","description":"Phase 2 of redaction: return the input file with detected PII removed (replace / mask / remove). Paid-tier only and credit-metered. Fail-closed: if the output cannot be verified clean, returns 500 with no file. Returns 503 when AI operations are not enabled, 403 for ineligible tiers, 402 when the monthly credit allotment is exhausted.","operationId":"redact_apply_api_v1_ai_redact_apply_post","parameters":[{"name":"X-API-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_redact_apply_api_v1_ai_redact_apply_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/contact":{"post":{"tags":["Contact"],"summary":"Submit Contact","operationId":"submit_contact_api_v1_contact_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/register":{"post":{"tags":["Auth"],"summary":"Register","operationId":"register_api_v1_auth_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/login":{"post":{"tags":["Auth"],"summary":"Login","operationId":"login_api_v1_auth_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/refresh":{"post":{"tags":["Auth"],"summary":"Refresh","operationId":"refresh_api_v1_auth_refresh_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/me":{"get":{"tags":["Auth"],"summary":"Me","operationId":"me_api_v1_auth_me_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/account/language":{"put":{"tags":["Auth"],"summary":"Set Preferred Language","description":"Set the language FileMorph sends this user's transactional email in.\n\nPersisted on ``users.preferred_lang``. Used wherever email is rendered\noutside an HTTP request (the dunning mail fired from a Stripe webhook)\nand as the first choice for request-context mail too (reset / resend /\ndeletion confirmation). It does **not** change the web-UI locale, which\nstays URL-prefix-driven — see ``app/core/i18n.py``.\n\nAuth: JWT bearer (``get_current_user``), like ``GET /auth/me``. Body:\n``{\"preferred_lang\": \"de\" | \"en\"}`` — an unsupported value is a 422\nfrom the Pydantic validator.","operationId":"set_preferred_language_api_v1_auth_account_language_put","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreferredLanguageRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/forgot-password":{"post":{"tags":["Auth"],"summary":"Forgot Password","operationId":"forgot_password_api_v1_auth_forgot_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/reset-password":{"post":{"tags":["Auth"],"summary":"Reset Password","operationId":"reset_password_api_v1_auth_reset_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/verify-email":{"post":{"tags":["Auth"],"summary":"Verify Email","description":"Consume a verification token and stamp ``email_verified_at``.\n\nIdempotent: re-verifying an already-verified email returns 200 with\na no-op confirmation. The ``eat`` claim binds the token to the\nuser's email at issuance time, so a token issued before an email\nrotation cannot auto-verify the new address.","operationId":"verify_email_api_v1_auth_verify_email_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyEmailRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/resend-verification":{"post":{"tags":["Auth"],"summary":"Resend Verification","description":"Send a fresh verification email to the authenticated user.\n\nAuth-required so the endpoint can't be used as a free email-spam\nvector. Returns 200 with a no-op message when the email is already\nverified (rather than 4xx) — the user-visible UX is \"we tried to\nhelp you\" regardless of state, and we never confirm verification\nstate of an arbitrary email to an unauthenticated caller.","operationId":"resend_verification_api_v1_auth_resend_verification_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/account":{"delete":{"tags":["Auth"],"summary":"Delete Account","description":"Self-service account deletion (DSGVO Art. 17).\n\nTwo paths, decided by ``deletion_mode_for(user)`` in\n``app/core/account_deletion.py``:\n\n* **free / never-paid** — full hard-delete of the ``users`` row;\n  PostgreSQL clears the related rows via the ON DELETE cascades.\n* **tax-retained** — the account has touched Stripe; German tax law\n  (HGB §257, AO §147) obliges a 10-year retention of the invoice →\n  customer link, permitted by DSGVO Art. 17(3)(b). The ``users`` row\n  is kept in a restricted state — only ``email`` / ``stripe_customer_id``\n  / ``tier`` / ``created_at`` survive; ``api_keys`` are still removed\n  and ``file_jobs`` / ``usage`` still anonymised. Any active Stripe\n  subscription is cancelled first; a Stripe error aborts the whole\n  thing with 500 and the account is unchanged. See\n  ``docs/gdpr-account-deletion-design.md`` § 5.B.\n\nAuth: JWT bearer only. ``X-API-Key`` is intentionally rejected — the\nAPI key is one of the things being deleted, so authenticating its\nown deletion has chicken-and-egg semantics. ``Depends(get_current_user)``\nreads only the ``Authorization`` header.\n\nRe-confirmation: three fields must match (``password``,\n``confirm_email``, ``confirm_word == 'DELETE'``). A ``password`` /\n``confirm_email`` mismatch returns the same generic 400 so a\nstolen-JWT attacker cannot probe which one was wrong; a bad\n``confirm_word`` is a Pydantic 422 before the body runs.","operationId":"delete_account_api_v1_auth_account_delete","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAccountRequest"}}}},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/keys":{"post":{"tags":["API Keys"],"summary":"Create Key","operationId":"create_key_api_v1_keys_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["API Keys"],"summary":"List Keys","operationId":"list_keys_api_v1_keys_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KeyResponse"},"title":"Response List Keys Api V1 Keys Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/keys/{key_id}":{"delete":{"tags":["API Keys"],"summary":"Delete Key","operationId":"delete_key_api_v1_keys__key_id__delete","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","title":"Key Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/billing/checkout/{tier}":{"post":{"tags":["Billing"],"summary":"Create Checkout Session","description":"Create a Stripe Checkout session for the given tier (pro | business).\n\nRequires `withdrawal_waiver_acknowledged: true` in the request body so the\nuser has explicitly waived their 14-day §312g BGB / §356 (5) BGB right of\nwithdrawal — the consent is recorded as a SHA-256 hash-chained audit event\nso it can be reproduced at dispute time.","operationId":"create_checkout_session_api_v1_billing_checkout__tier__post","parameters":[{"name":"tier","in":"path","required":true,"schema":{"type":"string","title":"Tier"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/billing/portal":{"post":{"tags":["Billing"],"summary":"Customer Portal","description":"Return a Stripe Billing Portal URL for the current user.","operationId":"customer_portal_api_v1_billing_portal_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/billing/cancellation":{"post":{"tags":["Billing"],"summary":"Submit Cancellation","description":"Cancel a Pro/Business contract online, without logging in (§ 312k BGB).\n\nConsumers must be able to cancel online without a login (OLG Köln\n6 U 62/24, OLG Nürnberg 3 U 2214/23, KG Berlin 5 UKl 10/25 and\n5 U 6/25), so no password can be asked for: the email address alone\nidentifies the account. The confirmation of content, date and time of\nreceipt and the end of the contract that § 312k (4) requires immediately\nin text form goes to the address stored on the matching account — its\nowner's mailbox is the only check there is — or, without exactly one\nmatching account, to the address entered. It is sent in every case but\na filled honeypot (below). The free-text reason is repeated in it only\nfor a matching account's own mailbox; anywhere else a fixed text stands\nin, or anyone could send their words to any address from our domain.\n\nAn ordinary cancellation \"at the earliest possible date\" for an address\nwith exactly one live subscription of the named plan is executed at once\n(Stripe ``cancel_at_period_end``). Every other case — no account or more\nthan one, extraordinary, a specific date, no, several or a different\nsubscription, a Stripe error, a filled honeypot — is emailed to the\noperator to handle.\n\nThe answer is the same whether or not the address belongs to an account\n(``received_at`` + ``email_sent``), so the form can't be used to find out\nwho is a customer. A received cancellation is never answered with an\nerror, and success is never reported for one that wasn't received.\n\nSpam: 10 requests per hour per IP, and the ``website`` honeypot. Unlike\n``/contact``, a filled honeypot is not dropped: a password manager may\nfill it on a real cancellation. It is received, audited and mailed to\nthe operator as suspected spam like any manual case — but nothing is\nchanged in Stripe and no confirmation goes to the entered address, so\nbots still can't use the form to mail strangers. The answer says\n``email_sent: false``, so the page tells the visitor to keep it as proof.\n\nAccepted residual risks:\n\n* Anyone who knows a customer's address can cancel that subscription at\n  the end of the billing period — asking for a password is what the law\n  rules out. The owner gets the confirmation mail and can undo the\n  cancellation in the Stripe customer portal until the period ends.\n* Without a matching account, the confirmation (minus the reason) goes to\n  whatever address was entered; the rate limit bounds how often.\n* Answering takes a little longer when Stripe is called, which hints\n  that an address has a paid subscription.","operationId":"submit_cancellation_api_v1_billing_cancellation_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancellationRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"Body_compress_batch_api_v1_compress_batch_post":{"properties":{"files":{"items":{"type":"string","contentMediaType":"application/octet-stream"},"type":"array","title":"Files","description":"Files to compress (multi-upload)"},"quality":{"anyOf":[{"type":"integer","maximum":100.0,"minimum":1.0},{"type":"null"}],"title":"Quality"},"target_size_kb":{"anyOf":[{"type":"integer","minimum":5.0},{"type":"null"}],"title":"Target Size Kb","description":"Maximum output size in KB, as for /compress. Applies to all files. JPEG/WebP/AVIF only."}},"type":"object","required":["files"],"title":"Body_compress_batch_api_v1_compress_batch_post"},"Body_compress_file_api_v1_compress_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"quality":{"anyOf":[{"type":"integer","maximum":100.0,"minimum":1.0},{"type":"null"}],"title":"Quality","description":"Quality 1 (smallest) - 100 (best). PNG and TIFF are lossless and ignore it."},"target_size_kb":{"anyOf":[{"type":"integer","minimum":5.0},{"type":"null"}],"title":"Target Size Kb","description":"Maximum output size in KB. Searches for the best quality that fits and scales the image down when even the lowest quality doesn't. Mutually exclusive with quality. JPEG/WebP/AVIF only."}},"type":"object","required":["file"],"title":"Body_compress_file_api_v1_compress_post"},"Body_convert_batch_api_v1_convert_batch_post":{"properties":{"files":{"items":{"type":"string","contentMediaType":"application/octet-stream"},"type":"array","title":"Files","description":"Files to convert (multi-upload)"},"target_formats":{"items":{"type":"string"},"type":"array","title":"Target Formats","description":"Target format per uploaded file, same order and length as `files`."},"quality":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Quality","default":85}},"type":"object","required":["files","target_formats"],"title":"Body_convert_batch_api_v1_convert_batch_post"},"Body_convert_file_api_v1_convert_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"target_format":{"type":"string","title":"Target Format","description":"Target format, e.g. 'jpg', 'pdf', 'mp3'"},"quality":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Quality","description":"Quality 1-100 (where applicable)","default":85}},"type":"object","required":["file","target_format"],"title":"Body_convert_file_api_v1_convert_post"},"Body_pdf_compress_api_v1_pdf_compress_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"target_kb":{"type":"integer","maximum":2097152.0,"exclusiveMinimum":0.0,"title":"Target Kb","description":"Maximum output size in KB. The PDF's embedded images are recompressed, and scaled down if needed, to fit; text/vector content and the layout are preserved."}},"type":"object","required":["file","target_kb"],"title":"Body_pdf_compress_api_v1_pdf_compress_post"},"Body_pdf_extract_api_v1_pdf_extract_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"pages":{"type":"string","title":"Pages","description":"1-based pages/ranges, e.g. '1-3,5'"}},"type":"object","required":["file","pages"],"title":"Body_pdf_extract_api_v1_pdf_extract_post"},"Body_pdf_insert_api_v1_pdf_insert_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"insert_file":{"type":"string","contentMediaType":"application/octet-stream","title":"Insert File"},"after_page":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"After Page","description":"Insert after this page (1-based); 0 inserts at the start. Leave it out to insert at the end."}},"type":"object","required":["file","insert_file"],"title":"Body_pdf_insert_api_v1_pdf_insert_post"},"Body_pdf_split_api_v1_pdf_split_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_pdf_split_api_v1_pdf_split_post"},"Body_redact_apply_api_v1_ai_redact_apply_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"entity_types":{"type":"string","title":"Entity Types","description":"Comma-separated types to redact; empty = all.","default":""},"mode":{"type":"string","title":"Mode","description":"replace | mask | remove","default":"replace"}},"type":"object","required":["file"],"title":"Body_redact_apply_api_v1_ai_redact_apply_post"},"Body_redact_detect_api_v1_ai_redact_detect_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"entity_types":{"type":"string","title":"Entity Types","description":"Comma-separated types to scan for; empty = all.","default":""}},"type":"object","required":["file"],"title":"Body_redact_detect_api_v1_ai_redact_detect_post"},"CancellationRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"contract":{"type":"string","enum":["pro","business"],"title":"Contract"},"kind":{"type":"string","enum":["ordinary","extraordinary"],"title":"Kind"},"reason":{"type":"string","maxLength":1000,"title":"Reason","default":""},"end":{"type":"string","enum":["earliest","date"],"title":"End"},"end_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"End Date"},"website":{"type":"string","maxLength":200,"title":"Website","default":""}},"type":"object","required":["email","contract","kind","end"],"title":"CancellationRequest","description":"Body schema for POST /billing/cancellation — the § 312k BGB\ncancellation form, submitted without login.\n\n``email`` identifies the account; the confirmation goes to the address\nstored on it (or, without an account, to this one). The validator\nnormalises the declaration so every consumer sees one shape: ``reason``\nonly survives an extraordinary cancellation (cleaned by\n``_clean_reason``), ``end_date`` only ``end == \"date\"``, where it is\nrequired and must not lie before yesterday (UTC) — the earliest date\nthat is still \"today\" somewhere, so a visitor behind UTC can pick their\nown today."},"CheckoutRequest":{"properties":{"withdrawal_waiver_acknowledged":{"type":"boolean","title":"Withdrawal Waiver Acknowledged","default":false}},"type":"object","title":"CheckoutRequest","description":"Body schema for POST /billing/checkout/{tier}.\n\nThe user must explicitly waive their 14-day right of withdrawal under\n§312g BGB / §356(5) BGB before paid-tier API access can be activated\nimmediately on Stripe checkout completion. Without this acknowledgement\nthe request is rejected with HTTP 400 — the standard 14-day withdrawal\nprotection then applies and immediate activation is deferred."},"ContactRequest":{"properties":{"name":{"type":"string","maxLength":120,"title":"Name","default":""},"email":{"type":"string","format":"email","title":"Email"},"subject":{"type":"string","maxLength":160,"title":"Subject","default":""},"message":{"type":"string","maxLength":5000,"minLength":20,"title":"Message"},"website":{"type":"string","maxLength":200,"title":"Website","default":""}},"type":"object","required":["email","message"],"title":"ContactRequest"},"CreateKeyRequest":{"properties":{"label":{"type":"string","maxLength":100,"title":"Label","default":"My API Key"}},"type":"object","title":"CreateKeyRequest"},"CreateKeyResponse":{"properties":{"id":{"type":"string","title":"Id"},"label":{"type":"string","title":"Label"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"last_used_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Used At"},"is_active":{"type":"boolean","title":"Is Active"},"key":{"type":"string","title":"Key"}},"type":"object","required":["id","label","created_at","last_used_at","is_active","key"],"title":"CreateKeyResponse"},"DeleteAccountRequest":{"properties":{"password":{"type":"string","maxLength":128,"title":"Password"},"confirm_email":{"type":"string","format":"email","title":"Confirm Email"},"confirm_word":{"type":"string","title":"Confirm Word"}},"type":"object","required":["password","confirm_email","confirm_word"],"title":"DeleteAccountRequest","description":"Three-field re-confirmation: each field defends a different mistake.\n\n* ``password`` — defends against a stolen JWT being used by someone\n  who doesn't know the password.\n* ``confirm_email`` — defends against the user being signed into the\n  wrong account.\n* ``confirm_word`` — defends against an accidental form submission;\n  must be the literal string ``DELETE``.\n\nSee ``docs/gdpr-account-deletion-design.md`` § 3 for the rationale."},"ForgotPasswordRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"}},"type":"object","required":["email"],"title":"ForgotPasswordRequest"},"FormatsResponse":{"properties":{"conversions":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object","title":"Conversions"},"compression":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object","title":"Compression"}},"type":"object","required":["conversions","compression"],"title":"FormatsResponse"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HealthResponse":{"properties":{"status":{"type":"string","title":"Status"}},"type":"object","required":["status"],"title":"HealthResponse"},"KeyResponse":{"properties":{"id":{"type":"string","title":"Id"},"label":{"type":"string","title":"Label"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"last_used_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Used At"},"is_active":{"type":"boolean","title":"Is Active"}},"type":"object","required":["id","label","created_at","last_used_at","is_active"],"title":"KeyResponse"},"LoginRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","maxLength":128,"title":"Password"}},"type":"object","required":["email","password"],"title":"LoginRequest"},"PreferredLanguageRequest":{"properties":{"preferred_lang":{"type":"string","title":"Preferred Lang"}},"type":"object","required":["preferred_lang"],"title":"PreferredLanguageRequest"},"RefreshRequest":{"properties":{"refresh_token":{"type":"string","title":"Refresh Token"}},"type":"object","required":["refresh_token"],"title":"RefreshRequest"},"RegisterRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","maxLength":128,"title":"Password"}},"type":"object","required":["email","password"],"title":"RegisterRequest"},"ResetPasswordRequest":{"properties":{"token":{"type":"string","title":"Token"},"new_password":{"type":"string","maxLength":128,"title":"New Password"}},"type":"object","required":["token","new_password"],"title":"ResetPasswordRequest"},"TokenResponse":{"properties":{"access_token":{"type":"string","title":"Access Token"},"refresh_token":{"type":"string","title":"Refresh Token"},"token_type":{"type":"string","title":"Token Type","default":"bearer"}},"type":"object","required":["access_token","refresh_token"],"title":"TokenResponse"},"UserResponse":{"properties":{"id":{"type":"string","title":"Id"},"email":{"type":"string","title":"Email"},"tier":{"type":"string","title":"Tier"},"role":{"type":"string","title":"Role"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"subscription_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subscription Status"},"preferred_lang":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Preferred Lang"}},"type":"object","required":["id","email","tier","role","created_at"],"title":"UserResponse"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerifyEmailRequest":{"properties":{"token":{"type":"string","title":"Token"}},"type":"object","required":["token"],"title":"VerifyEmailRequest"}}}}