{"openapi":"3.1.0","info":{"title":"Tlamakatl API","version":"1.0.0"},"paths":{"/api/auth/signup":{"post":{"tags":["Auth"],"summary":"Signup","description":"Create a new user + immediate session cookie.","operationId":"signup_api_auth_signup_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Signup Api Auth Signup Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/login":{"post":{"tags":["Auth"],"summary":"Login","description":"Authenticate + issue a session cookie.\n\nConstant-error-message on bad credentials so an attacker can't tell\n\"no such email\" from \"wrong password\".","operationId":"login_api_auth_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Login Api Auth Login Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/logout":{"post":{"tags":["Auth"],"summary":"Logout","description":"Revoke the current session (if any) and clear the cookie.","operationId":"logout_api_auth_logout_post","responses":{"204":{"description":"Successful Response"}}}},"/api/auth/me":{"get":{"tags":["Auth"],"summary":"Me","operationId":"me_api_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}}}}},"/api/auth/verify-email/request":{"post":{"tags":["Auth"],"summary":"Request Email Verification","description":"Anti-enumeration: always 202 regardless of whether the email exists.\n\nIf a verified user requests this, we still return 202 but don't send\nanything. This avoids \"your email is verified\" leaks too.","operationId":"request_email_verification_api_auth_verify_email_request_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResendVerificationRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Request Email Verification Api Auth Verify Email Request Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/verify-email/confirm":{"post":{"tags":["Auth"],"summary":"Confirm Email Verification","operationId":"confirm_email_verification_api_auth_verify_email_confirm_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyEmailConfirmRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Confirm Email Verification Api Auth Verify Email Confirm Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/verify-email/resend":{"post":{"tags":["Auth"],"summary":"Resend Verification For Self","description":"Logged-in self-service resend. Same anti-enumeration 202.","operationId":"resend_verification_for_self_api_auth_verify_email_resend_post","responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Resend Verification For Self Api Auth Verify Email Resend Post"}}}}}}},"/api/auth/forgot-password":{"post":{"tags":["Auth"],"summary":"Forgot Password","description":"Always 202. Sends an email only if the address belongs to an active user.\n\nEXEC-023 — IP throttle. /forgot-password is a free email-send\nprimitive: without rate limiting an attacker can use the legitimate\nResend pipeline to spam any address, OR can enumerate which addresses\nbounce (timing oracle). We reuse the login IP throttle counter\nrather than maintain a separate one — the threat model (single IP\nmaking many sensitive auth requests) is the same, and the limit\n(20/15min) is loose enough that even a busy office is fine.","operationId":"forgot_password_api_auth_forgot_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Forgot Password Api Auth Forgot Password Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/reset-password":{"post":{"tags":["Auth"],"summary":"Reset Password","operationId":"reset_password_api_auth_reset_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Reset Password Api Auth Reset Password Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/keys":{"get":{"tags":["API Keys"],"summary":"List Keys","operationId":"list_keys_api_keys_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ApiKeyView"},"type":"array","title":"Response List Keys Api Keys Get"}}}}}},"post":{"tags":["API Keys"],"summary":"Create Key","operationId":"create_key_api_keys_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreateRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/keys/{key_id}":{"delete":{"tags":["API Keys"],"summary":"Revoke Key","operationId":"revoke_key_api_keys__key_id__delete","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"integer","title":"Key Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/usage":{"get":{"tags":["API Keys"],"summary":"Get Usage","description":"Return current window counts + remaining budget for the caller.\n\nDoesn't require a user session — anonymous callers see their own\nbucket. The IdentityMiddleware has already done the work; we just\ninspect ``request.state.identity`` and consult the limiter without\nrecording another hit.","operationId":"get_usage_api_usage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResponse"}}}}}}},"/api/upload":{"post":{"tags":["Gateway"],"summary":"Upload File","description":"Upload a file and create a processing job.\n\nThis endpoint:\n1. Validates the file type and size\n2. Uploads the file to shared storage\n3. Creates a job record\n4. Enqueues the job for processing\n5. Returns the job ID immediately\n\nArgs:\n    file: The file to upload\n    job_type: Type of processing job (e.g., 'pdf-split', 'image-resize')\n    parameters: JSON string of job-specific parameters (optional)\n\nReturns:\n    JobCreateResponse with job_id and status\n\nRaises:\n    HTTPException: If validation fails or upload errors occur","operationId":"upload_file_api_upload_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_file_api_upload_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCreateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/jobs":{"get":{"tags":["Gateway"],"summary":"List User Jobs","description":"List jobs for the authenticated user with pagination.","operationId":"list_user_jobs_api_jobs_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/jobs/{job_id}":{"get":{"tags":["Gateway"],"summary":"Get Job Status","description":"Query the status of a job.\n\nArgs:\n    job_id: Unique job identifier\n\nReturns:\n    JobStatusResponse with current job status and details\n\nRaises:\n    HTTPException: If job not found","operationId":"get_job_status_api_jobs__job_id__get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/download/{file_id}":{"get":{"tags":["Gateway"],"summary":"Download File","description":"Download a file by streaming it through the gateway.\n\nWe stream rather than 302-redirect to a presigned MinIO URL because the\npresigned URL is signed for the internal hostname (e.g. minio:9000) and a\nhost-side browser cannot resolve that. Streaming through the gateway keeps\nthe download path identical locally and behind a public reverse proxy.\n\nEXEC-004 tenant isolation:\n  * ``users/<id>/<uuid>.ext`` → only the same authenticated user (session\n    or API key) may download.\n  * ``anon/<uuid>.ext`` → anonymous callers OK (the file was uploaded\n    without a user identity).\n  * Legacy flat ``<uuid>.ext`` → backward-compat: served, but logged as\n    a deprecation warning so the next sweep can backfill ownership.\n\nArgs:\n    file_id: Object name in storage. May include a ``users/<id>/`` or\n        ``anon/`` prefix (URL-decoded by FastAPI's ``:path`` matcher).\n    filename: Optional original filename used to set Content-Disposition.\n\nReturns:\n    Streamed file response.\n\nRaises:\n    HTTPException: If file not found, ownership mismatch, or storage read fails.","operationId":"download_file_api_download__file_id__get","parameters":[{"name":"file_id","in":"path","required":true,"schema":{"type":"string","title":"File Id"}},{"name":"filename","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Original filename for extension","title":"Filename"},"description":"Original filename for extension"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cleanup":{"post":{"tags":["Gateway"],"summary":"Trigger Cleanup","description":"Manually trigger cleanup of old and marked files.\n\nThis endpoint runs a full cleanup cycle:\n1. Deletes files older than TTL (15 minutes)\n2. Deletes files marked for deletion (downloaded files)\n3. Checks storage usage and logs warnings if threshold exceeded\n\nReturns:\n    Cleanup statistics including number of files deleted","operationId":"trigger_cleanup_api_cleanup_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/preview/pdf":{"post":{"tags":["Preview"],"summary":"Preview Pdf","description":"Upload a PDF and render small thumbnails for each page.\n\nEXEC-011 — stream-validate the upload to avoid buffering the whole\nfile before the size + magic-byte gates fire. Previous behaviour\n``data = await file.read()`` allocated the full payload (up to\nMAX_FILE_SIZE_BYTES, default 50MB) before any validation, which both\nspikes memory and gives an attacker free RAM if the limit is raised.\n\nFlow now:\n\n  1. Read first 16 bytes, check ``%PDF-`` magic, reject early if\n     missing — no body work for non-PDFs.\n  2. Stream the rest of the upload through a SpooledTemporaryFile\n     (in-memory until 4MB, then on disk) so very large PDFs don't\n     live entirely in RAM.\n  3. Track accumulated size on the fly and 413 the moment we exceed\n     MAX_FILE_SIZE_BYTES — a 5GB upload disconnects ~50MB in.\n  4. Hand the spool to MinIO upload + PyMuPDF; both APIs accept a\n     file-like object.\n\nThe original PDF is persisted to MinIO so the caller can later submit\na job that operates on it without re-uploading. Page thumbnails are\nrendered inline (PyMuPDF is fast on small/medium PDFs) and stored\nalongside under the `preview/{file_id}/` prefix.","operationId":"preview_pdf_api_preview_pdf_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_preview_pdf_api_preview_pdf_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/process":{"post":{"tags":["Preview"],"summary":"Process From Preview","description":"Queue a job using files already uploaded via /api/preview/pdf.\n\nNo multipart upload here — the request body is JSON and references the\nstorage URLs returned by the preview endpoint. This is the path the\nrich PDF tool UI takes after the user has reviewed pages.","operationId":"process_from_preview_api_process_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCreateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/pdf-split":{"post":{"tags":["PDF"],"summary":"Split Pdf","operationId":"split_pdf_api_pdf_split_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_split_pdf_api_pdf_split_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/pdf-merge":{"post":{"tags":["PDF"],"summary":"Merge Pdfs","operationId":"merge_pdfs_api_pdf_merge_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_merge_pdfs_api_pdf_merge_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/image-resize":{"post":{"tags":["Image"],"summary":"Resize Image","operationId":"resize_image_api_image_resize_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_resize_image_api_image_resize_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/image-compress":{"post":{"tags":["Image"],"summary":"Compress Image","operationId":"compress_image_api_image_compress_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_compress_image_api_image_compress_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/json-formatter":{"post":{"tags":["Text"],"summary":"Format Json","operationId":"format_json_api_json_formatter_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TextInput"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/json-validator":{"post":{"tags":["Text"],"summary":"Validate Json","operationId":"validate_json_api_json_validator_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TextInput"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/text-case-converter":{"post":{"tags":["Text"],"summary":"Convert Case","operationId":"convert_case_api_text_case_converter_post","parameters":[{"name":"case_type","in":"query","required":false,"schema":{"type":"string","default":"snake","title":"Case Type"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TextInput"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/base64":{"post":{"tags":["Utility"],"summary":"Base64 Operation","operationId":"base64_operation_api_base64_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Base64Input"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/uuid-generator":{"get":{"tags":["Utility"],"summary":"Generate Uuid","operationId":"generate_uuid_api_uuid_generator_get","parameters":[{"name":"count","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Count"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/password-generator":{"get":{"tags":["Utility"],"summary":"Generate Password","operationId":"generate_password_api_password_generator_get","parameters":[{"name":"length","in":"query","required":false,"schema":{"type":"integer","default":16,"title":"Length"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"summary":"Read Root","operationId":"read_root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"summary":"Health Check","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/healthz":{"get":{"summary":"Healthz","description":"Liveness probe — process is up.","operationId":"healthz_healthz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/readyz":{"get":{"summary":"Readyz","description":"Readiness probe — DB + Redis + MinIO reachable.","operationId":"readyz_readyz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/metrics":{"get":{"summary":"Metrics","description":"Prometheus scrape endpoint.\n\nEXEC-027 — restrict by CIDR allow-list to avoid leaking internal\nseries (per-route latency, per-user job counts, error rates) to the\nopen internet. Operationally we either deploy this behind nginx with\na ``location /metrics { allow 10.0.0.0/8; deny all; }`` block, OR\nset ``METRICS_ALLOW_CIDRS`` to a comma-separated list of CIDRs the\nPrometheus server is allowed to scrape from.\n\nDefault (empty allow-list) keeps the existing behaviour so local\ndev still works without configuration. In any non-development\nenvironment, leaving METRICS_ALLOW_CIDRS unset OR setting it to a\nlist that excludes the scraper IP both lock the endpoint down to\n127.0.0.1 by default.","operationId":"metrics_metrics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"ApiKeyCreateRequest":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Name"},"env":{"type":"string","pattern":"^(live|test)$","title":"Env","default":"live"}},"type":"object","title":"ApiKeyCreateRequest"},"ApiKeyCreateResponse":{"properties":{"id":{"type":"integer","title":"Id"},"prefix":{"type":"string","title":"Prefix"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"tier":{"type":"string","title":"Tier"},"is_active":{"type":"boolean","title":"Is Active"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"last_used_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Used At"},"revoked_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Revoked At"},"key":{"type":"string","title":"Key","description":"The full API key — copy now; not shown again."}},"type":"object","required":["id","prefix","name","tier","is_active","created_at","last_used_at","revoked_at","key"],"title":"ApiKeyCreateResponse"},"ApiKeyView":{"properties":{"id":{"type":"integer","title":"Id"},"prefix":{"type":"string","title":"Prefix"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"tier":{"type":"string","title":"Tier"},"is_active":{"type":"boolean","title":"Is Active"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"last_used_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Used At"},"revoked_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Revoked At"}},"type":"object","required":["id","prefix","name","tier","is_active","created_at","last_used_at","revoked_at"],"title":"ApiKeyView"},"Base64Input":{"properties":{"text":{"type":"string","title":"Text"},"operation":{"type":"string","title":"Operation"}},"type":"object","required":["text","operation"],"title":"Base64Input"},"Body_compress_image_api_image_compress_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"},"quality":{"type":"integer","title":"Quality","default":85}},"type":"object","required":["file"],"title":"Body_compress_image_api_image_compress_post"},"Body_merge_pdfs_api_pdf_merge_post":{"properties":{"files":{"items":{"type":"string","format":"binary"},"type":"array","title":"Files"}},"type":"object","required":["files"],"title":"Body_merge_pdfs_api_pdf_merge_post"},"Body_preview_pdf_api_preview_pdf_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_preview_pdf_api_preview_pdf_post"},"Body_resize_image_api_image_resize_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"},"width":{"type":"integer","title":"Width"},"height":{"type":"integer","title":"Height"}},"type":"object","required":["file","width","height"],"title":"Body_resize_image_api_image_resize_post"},"Body_split_pdf_api_pdf_split_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_split_pdf_api_pdf_split_post"},"Body_upload_file_api_upload_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"},"job_type":{"type":"string","title":"Job Type"},"parameters":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parameters"}},"type":"object","required":["file","job_type"],"title":"Body_upload_file_api_upload_post"},"ForgotPasswordRequest":{"properties":{"email":{"type":"string","maxLength":254,"minLength":3,"title":"Email"}},"type":"object","required":["email"],"title":"ForgotPasswordRequest"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"JobCreateResponse":{"properties":{"job_id":{"type":"string","title":"Job Id","description":"Unique job identifier"},"status":{"type":"string","title":"Status","description":"Initial job status"},"message":{"type":"string","title":"Message","description":"Success message"},"ws_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ws Token","description":"Short-lived token for the WebSocket /ws/<job_id> handshake"}},"type":"object","required":["job_id","status","message"],"title":"JobCreateResponse","description":"Response model for job creation"},"JobStatusResponse":{"properties":{"job_id":{"type":"string","title":"Job Id"},"job_type":{"type":"string","title":"Job Type"},"status":{"type":"string","title":"Status"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Completed At"},"output_file_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Output File Url"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"}},"type":"object","required":["job_id","job_type","status","created_at"],"title":"JobStatusResponse","description":"Response model for job status query"},"LoginRequest":{"properties":{"email":{"type":"string","maxLength":254,"minLength":3,"title":"Email"},"password":{"type":"string","maxLength":1024,"minLength":1,"title":"Password"}},"type":"object","required":["email","password"],"title":"LoginRequest"},"MeResponse":{"properties":{"user_id":{"type":"integer","title":"User Id"},"email":{"type":"string","title":"Email"},"tier":{"type":"string","title":"Tier"},"is_active":{"type":"boolean","title":"Is Active"},"email_verified":{"type":"boolean","title":"Email Verified","default":true}},"type":"object","required":["user_id","email","tier","is_active"],"title":"MeResponse"},"MergeFileSpec":{"properties":{"storage_url":{"type":"string","title":"Storage Url","description":"Internal s3:// URL returned by /api/preview/pdf"},"pages":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Pages","description":"Optional 1-indexed page list to include from this file. Null = include all pages."},"original_filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Original Filename"}},"type":"object","required":["storage_url"],"title":"MergeFileSpec","description":"One PDF in a merge job. `pages` is optional — null means all pages."},"PreviewPage":{"properties":{"page_number":{"type":"integer","title":"Page Number","description":"1-indexed page number"},"preview_url":{"type":"string","title":"Preview Url","description":"Public URL of the thumbnail PNG"},"width":{"type":"integer","title":"Width"},"height":{"type":"integer","title":"Height"}},"type":"object","required":["page_number","preview_url","width","height"],"title":"PreviewPage"},"PreviewResponse":{"properties":{"file_id":{"type":"string","title":"File Id"},"original_filename":{"type":"string","title":"Original Filename"},"storage_url":{"type":"string","title":"Storage Url","description":"Internal s3:// URL for downstream jobs"},"page_count":{"type":"integer","title":"Page Count"},"truncated":{"type":"boolean","title":"Truncated","description":"True if the PDF had more pages than PREVIEW_MAX_PAGES and only the first N were rendered as previews.","default":false},"pages":{"items":{"$ref":"#/components/schemas/PreviewPage"},"type":"array","title":"Pages"}},"type":"object","required":["file_id","original_filename","storage_url","page_count","pages"],"title":"PreviewResponse"},"ProcessRequest":{"properties":{"job_type":{"type":"string","title":"Job Type"},"storage_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Storage Url","description":"Required for single-file jobs (pdf-split, pdf-remove-pages, pdf-compress, pdf-to-images). Use storage_url returned by /api/preview."},"parameters":{"additionalProperties":true,"type":"object","title":"Parameters"},"files":{"anyOf":[{"items":{"$ref":"#/components/schemas/MergeFileSpec"},"type":"array"},{"type":"null"}],"title":"Files"}},"type":"object","required":["job_type"],"title":"ProcessRequest","description":"JSON request to /api/process — no file upload, references prior previews."},"ResendVerificationRequest":{"properties":{"email":{"type":"string","maxLength":254,"minLength":3,"title":"Email"}},"type":"object","required":["email"],"title":"ResendVerificationRequest"},"ResetPasswordRequest":{"properties":{"token":{"type":"string","maxLength":512,"minLength":20,"title":"Token"},"new_password":{"type":"string","maxLength":1024,"minLength":12,"title":"New Password"}},"type":"object","required":["token","new_password"],"title":"ResetPasswordRequest"},"SignupRequest":{"properties":{"email":{"type":"string","maxLength":254,"minLength":3,"title":"Email"},"password":{"type":"string","maxLength":1024,"minLength":12,"title":"Password"}},"type":"object","required":["email","password"],"title":"SignupRequest"},"TextInput":{"properties":{"text":{"type":"string","title":"Text"}},"type":"object","required":["text"],"title":"TextInput"},"UsageResponse":{"properties":{"identity_kind":{"type":"string","title":"Identity Kind"},"tier":{"type":"string","title":"Tier"},"windows":{"items":{"$ref":"#/components/schemas/UsageWindow"},"type":"array","title":"Windows"}},"type":"object","required":["identity_kind","tier","windows"],"title":"UsageResponse"},"UsageWindow":{"properties":{"label":{"type":"string","title":"Label"},"seconds":{"type":"integer","title":"Seconds"},"limit":{"type":"integer","title":"Limit"},"count":{"type":"integer","title":"Count"},"remaining":{"type":"integer","title":"Remaining"}},"type":"object","required":["label","seconds","limit","count","remaining"],"title":"UsageWindow"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerifyEmailConfirmRequest":{"properties":{"token":{"type":"string","maxLength":512,"minLength":20,"title":"Token"}},"type":"object","required":["token"],"title":"VerifyEmailConfirmRequest"}}}}