{"openapi":"3.1.0","info":{"title":"GlassyPic API","description":"AI-powered image optimization API","version":"1.0.0"},"paths":{"/health":{"get":{"tags":["health"],"summary":"Health Check","description":"Health check endpoint for Cloud Run and monitoring.\nReturns 200 OK if the service is healthy.","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness Check","description":"Readiness check for Cloud Run. Probes database and GCS connectivity.\nReturns 503 with per-check details if any dependency is unavailable.\n\nEach check is wrapped in asyncio.to_thread (Supabase client is synchronous)\nand asyncio.wait_for so a stalled dependency returns a fast 503 rather than\ntying up the event loop.\n\nGCS is probed via list_blobs(max_results=1) — this exercises\nstorage.objects.list (within roles/storage.objectAdmin) rather than\nstorage.buckets.get (not granted by objectAdmin, which bucket.exists() needs).\n\nExternal API availability (TinyPNG, Replicate, etc.) is intentionally\nexcluded — their outages should not mark this instance unready.","operationId":"readiness_check_readiness_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/upload":{"post":{"tags":["upload"],"summary":"Upload File","description":"Upload a single file to temporary storage (Phase 1 of two-phase upload).\n\nThis endpoint ONLY stores the file - NO processing occurs here.\nUse /auto, /compress, /resize, /upscale, or /tag endpoints to process uploaded files.\n\nReturns:\n    temp_file_id: Use this ID when calling processing endpoints","operationId":"upload_file_upload_post","parameters":[{"name":"x-session-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Guest session token","title":"X-Session-Token"},"description":"Guest session token"},{"name":"x-forwarded-for","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"User IP address","title":"X-Forwarded-For"},"description":"User IP address"}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_file_upload_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TempFileResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"options":{"tags":["upload"],"summary":"Upload Options","description":"Handle CORS preflight requests.","operationId":"upload_options_upload_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/upload/url":{"post":{"tags":["upload"],"summary":"Upload From Url","description":"Upload an image from a URL to temporary storage (Phase 1 of two-phase upload).\n\nThe backend fetches the image directly from the URL, validates it,\nand stores it in temp storage — same result as POST /upload.","operationId":"upload_from_url_upload_url_post","parameters":[{"name":"x-session-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Guest session token","title":"X-Session-Token"},"description":"Guest session token"},{"name":"x-forwarded-for","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"User IP address","title":"X-Forwarded-For"},"description":"User IP address"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UrlUploadRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TempFileResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/upload/signed-url":{"post":{"tags":["upload"],"summary":"Get Signed Upload Url","description":"Phase 1a of Track 1 (browser direct) upload.\n\nReturns a GCS signed PUT URL the browser uses to upload directly,\nbypassing the API server for the file bytes.","operationId":"get_signed_upload_url_upload_signed_url_post","parameters":[{"name":"x-session-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Session-Token"}},{"name":"x-forwarded-for","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-For"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignedUploadUrlRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignedUploadUrlResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"options":{"tags":["upload"],"summary":"Upload Options","description":"Handle CORS preflight requests.","operationId":"upload_options_upload_signed_url_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/upload/confirm":{"post":{"tags":["upload"],"summary":"Confirm Upload","description":"Phase 1c of Track 1 (browser direct) upload.\n\nCalled after the browser has PUT the file to GCS. Reads the first 64 bytes\nvia a Range request for format detection (HEIC, GIF, SVG, ICO, TIFF, BMP),\nvalidates size, writes GCS object metadata, and returns TempFileResponse\nidentical in shape to POST /upload.","operationId":"confirm_upload_upload_confirm_post","parameters":[{"name":"x-session-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Session-Token"}},{"name":"x-forwarded-for","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-For"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmUploadRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TempFileResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"options":{"tags":["upload"],"summary":"Upload Options","description":"Handle CORS preflight requests.","operationId":"upload_options_upload_confirm_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/auto":{"post":{"tags":["auto"],"summary":"Process Auto","description":"Process previously uploaded temp files with smart routing.\n\nThis is the Phase 2 endpoint for the homepage. It accepts temp_file_ids\nfrom the /upload endpoint and applies processing based on settings.\n\nComponents are automatically determined by settings:\n- Upscale: if output_upscale_factor is set\n- Resize: if output_width or output_height is set\n- Compress: always runs (quality setting)\n- Tag: if output_seo_tag_gen is True\n\nIdempotency (latency spec §Correlation contract): when body.request_id is\nset, (actor, request_id) is reserved before any payment or job creation\nruns. A retry with the same request_id and an equivalent fingerprint\n(same files+settings) replays the stored response instead of re-running\npayment. A retry with a different fingerprint under a reused request_id\ngets 409. Callers that don't send request_id get no enforcement — this is\nthe pre-existing behavior, preserved for backward compatibility.\n\nRecovery: job creation is checkpointed into the reservation the instant\nreal job IDs exist (before x402 post-settlement linking/verification), so\na retry that lands after a crash or a post-settlement failure replays\nthose job IDs instead of a dead-end 409 — see the checkpoint write in\n_process_auto_inner and the pending-with-snapshot branch below. A crash\nBEFORE that checkpoint (mid process_temp_files) falls back to\nreconstructing the response from processing_jobs.batch_id directly — each\njob row is durably committed the instant its own db.create_job() call\nreturns, independent of the checkpoint.\n\nResidual gap (code review 2026-07-25, scoped deferral not attempted here):\na crash after x402 settles but before the FIRST job insert still leaves\nthe reservation permanently pending (409 forever) — and that window isn't\nnegligible: process_temp_files re-fetches each file's info from GCS\n(_process_single_temp_file's own get_temp_file_info call, redundant with\nbut separate from the preflight check earlier) before its own\ndb.create_job() call, so a real network round-trip sits between\n\"payment settled\" and \"first job row committed.\"\n\nThe fix is already designed — do NOT design a new one, and in particular\ndo not reach for the transactional-outbox shape (inserting job rows\nbefore settlement) that an earlier draft of this comment proposed. The\nlatency spec (docs/superpowers/specs/\n2026-07-17-polish-first-download-latency-architecture-design.md) names\nthis exact failure and decided against that in eng review: per §445, a\ndurable settlement record linking request_id, the normalized fingerprint,\nand the payment reference is written AT OR BEFORE settlement, so a crash\nbetween settlement and batch creation always leaves an authoritative\nrecord; per §429, replay then reconstructs or resumes the same batch from\nthe settled transaction and never accepts a second payment.\n\nThat mechanism is strictly less invasive than an outbox: the settlement\nrecord — not row-existence — is the recovery anchor, so no job row has to\nexist at crash time and payment is never reordered against job creation.\nIt therefore PRESERVES the current guarantee that a settlement failure\nafter preflight is caught before any job row exists, with nothing to\nreconcile. Closing this gap is part of implementing latency spec §2\n(durable batch creation) + §445, not separate work — see docs/ROADMAP.md\nunder \"Idempotent /auto batch creation\".","operationId":"process_auto_auto_post","parameters":[{"name":"x-session-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Guest session token","title":"X-Session-Token"},"description":"Guest session token"},{"name":"x-forwarded-for","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"User IP address","title":"X-Forwarded-For"},"description":"User IP address"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessFilesRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessFilesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"options":{"tags":["auto"],"summary":"Process Auto Options","description":"Handle CORS preflight requests.","operationId":"process_auto_options_auto_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/status/stream-token":{"post":{"tags":["status"],"summary":"Create Stream Token","description":"Mint a short-lived signed token for SSE auth (dual-channel D2).\n\nThe browser's native EventSource cannot send headers, so it calls this\nauthed endpoint, then opens the SSE URL with `?stream_token=<token>`. The\ntoken carries only the actor + expiry, never the session/JWT itself.","operationId":"create_stream_token_status_stream_token_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"options":{"tags":["status"],"summary":"Status Options","description":"Handle CORS preflight requests.","operationId":"status_options_status_stream_token_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/status/stream":{"get":{"tags":["status"],"summary":"Stream Batch Status","description":"Stream status updates for multiple jobs via SSE.\n\nActor-scoped: only jobs owned by the caller are streamed. Auth is\ndual-channel (X-Session-Token / Authorization header, or a `stream_token`\nquery param for native EventSource). 403 if the caller owns none of them.\n\nQuery params:\n- job_ids: List of job UUIDs to monitor\n- stream_token: signed token (native EventSource path)\n\nEvents:\n- `status`: Individual job status update\n- `complete`: Individual job reached terminal state\n- `done`: All jobs completed\n- `error`: Error occurred\n- `timeout`: Connection timeout","operationId":"stream_batch_status_status_stream_get","parameters":[{"name":"job_ids","in":"query","required":true,"schema":{"type":"array","items":{"type":"string","format":"uuid"},"description":"List of job IDs to stream","title":"Job Ids"},"description":"List of job IDs to stream"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"options":{"tags":["status"],"summary":"Status Options","description":"Handle CORS preflight requests.","operationId":"status_options_status_stream_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/status/{job_id}/stream":{"get":{"tags":["status"],"summary":"Stream Status","description":"Stream job status updates via Server-Sent Events (SSE).\n\nActor-scoped: the caller must own the job. Auth is dual-channel (header, or\na `stream_token` query param for native EventSource). 403 for non-owners.\n\nBenefits over polling:\n- Instant updates when status changes (~10ms vs ~250ms avg polling delay)\n- No wasted requests while waiting\n- Automatic reconnection via SSE retry mechanism\n\nEvents:\n- `status`: Job status update (queued, processing)\n- `complete`: Terminal state reached (completed, failed, expired)\n- `error`: Error occurred\n- `timeout`: Connection timeout (5 min max)\n\nExample client usage:\n```javascript\n// Mint a stream token first (POST /status/stream-token with auth), then:\nconst eventSource = new EventSource(`/status/123/stream?stream_token=${t}`);\neventSource.addEventListener('complete', (e) => {\n  console.log(JSON.parse(e.data));\n  eventSource.close();\n});\n```","operationId":"stream_status_status__job_id__stream_get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"options":{"tags":["status"],"summary":"Status Options","description":"Handle CORS preflight requests.","operationId":"status_options_status__job_id__stream_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/status/{job_id}":{"get":{"tags":["status"],"summary":"Get Status","description":"Get the status of a single job.\n\nActor-scoped: the caller must own the job (guest session or authenticated\nuser). A non-owner receives 403 with no disclosure of whether the job exists.\n\nReturns:\n- Job status (queued, processing, completed, failed, expired)\n- Position in queue (if queued)\n- Estimated wait time (if queued)\n- Download URL (if completed)\n- Error message (if failed)\n- Processing results (compression ratio, file sizes, metadata)","operationId":"get_status_status__job_id__get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"options":{"tags":["status"],"summary":"Status Options","description":"Handle CORS preflight requests.","operationId":"status_options_status__job_id__options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/status":{"get":{"tags":["status"],"summary":"Get Batch Status","description":"Get the status of multiple jobs in a batch.\n\nActor-scoped: only jobs owned by the caller are returned. Foreign job IDs\nare silently dropped rather than 403-ing the whole batch, so one bad ID\ncan't deny status for the caller's own jobs.\n\nQuery params:\n- job_ids: Comma-separated list of job UUIDs\n\nReturns list of job statuses (owned only).","operationId":"get_batch_status_status_get","parameters":[{"name":"job_ids","in":"query","required":true,"schema":{"type":"array","items":{"type":"string","format":"uuid"},"description":"List of job IDs to check","title":"Job Ids"},"description":"List of job IDs to check"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"options":{"tags":["status"],"summary":"Status Options","description":"Handle CORS preflight requests.","operationId":"status_options_status_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/download/{job_id}":{"get":{"tags":["download"],"summary":"Download File","description":"Download a processed image file.\n\n- Verifies job is completed\n- Checks job is not expired\n- Streams file from GCS\n- Sets appropriate headers for download\n- Returns 404 if file not found or expired","operationId":"download_file_download__job_id__get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"options":{"tags":["download"],"summary":"Download Options","description":"Handle CORS preflight requests.","operationId":"download_options_download__job_id__options","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/download/{job_id}/url":{"get":{"tags":["download"],"summary":"Get Download Url","description":"Get a pre-signed URL for direct download from GCS.\n\nThis is faster than the streaming endpoint because:\n- Client downloads directly from GCS (no API server proxy)\n- Reduces latency by ~50-150ms\n- Reduces API server CPU/bandwidth usage\n\nThe URL is valid for 15 minutes.\n\nReturns:\n- url: Pre-signed GCS URL for direct download\n- filename: Suggested filename for the download\n- content_type: MIME type of the file\n- size: File size in bytes\n- expires_in: URL validity in seconds","operationId":"get_download_url_download__job_id__url_get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignedUrlResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"options":{"tags":["download"],"summary":"Download Options","description":"Handle CORS preflight requests.","operationId":"download_options_download__job_id__url_options","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/process":{"post":{"tags":["processing"],"summary":"Process Job","description":"Process an image job through the 4-component pipeline.\n\nPipeline order: Upscale → Resize → Compress → Tag\nComponents are determined by settings values (not run_* flags).\n\nArgs:\n    request: Job processing request with job_id, input_path, settings\n    token_claims: Verified OIDC token claims (injected by dependency)\n\nReturns:\n    Dict with processing results","operationId":"process_job_process_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-cloudtasks-taskname","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Cloudtasks-Taskname"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessJobRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"options":{"tags":["processing"],"summary":"Process Options","description":"Handle CORS preflight requests.","operationId":"process_options_process_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/compress":{"post":{"tags":["compress"],"summary":"Compress Images","description":"Compress images with optional format conversion.\n\nAccepts temp_file_ids from the /upload endpoint.","operationId":"compress_images_compress_post","parameters":[{"name":"x-session-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Guest session token","title":"X-Session-Token"},"description":"Guest session token"},{"name":"x-forwarded-for","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"User IP address","title":"X-Forwarded-For"},"description":"User IP address"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompressRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessFilesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/resize":{"post":{"tags":["resize"],"summary":"Resize Images","description":"Resize images to specified dimensions.\n\nAccepts temp_file_ids from the /upload endpoint.","operationId":"resize_images_resize_post","parameters":[{"name":"x-session-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Guest session token","title":"X-Session-Token"},"description":"Guest session token"},{"name":"x-forwarded-for","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"User IP address","title":"X-Forwarded-For"},"description":"User IP address"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResizeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessFilesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/upscale":{"post":{"tags":["upscale"],"summary":"Upscale Images","description":"Upscale images using AI (2x or 4x).\n\nAccepts temp_file_ids from the /upload endpoint.","operationId":"upscale_images_upscale_post","parameters":[{"name":"x-session-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Guest session token","title":"X-Session-Token"},"description":"Guest session token"},{"name":"x-forwarded-for","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"User IP address","title":"X-Forwarded-For"},"description":"User IP address"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpscaleRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessFilesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/tag":{"post":{"tags":["tag"],"summary":"Tag Images","description":"Generate AI metadata for images (alt text, keywords, filename).\n\nAccepts temp_file_ids from the /upload endpoint.","operationId":"tag_images_tag_post","parameters":[{"name":"x-session-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Guest session token","title":"X-Session-Token"},"description":"Guest session token"},{"name":"x-forwarded-for","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"User IP address","title":"X-Forwarded-For"},"description":"User IP address"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessFilesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/config":{"get":{"tags":["config","config"],"summary":"Get Public Config","description":"Get public configuration values.\n\nReturns tier limits and other non-sensitive config.","operationId":"get_public_config_config_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/billing/checkout":{"post":{"tags":["billing","billing"],"summary":"Create Checkout","description":"Create a Stripe Checkout session for subscription upgrade.\n\nEnforces single subscription per user:\n- Users with active subscription must use billing portal to change plans\n- Users can only create new checkout if no active subscription exists","operationId":"create_checkout_billing_checkout_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/portal":{"post":{"tags":["billing","billing"],"summary":"Create Portal","description":"Create a Stripe Billing Portal session.","operationId":"create_portal_billing_portal_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortalResponse"}}}}}}},"/billing/subscription":{"get":{"tags":["billing","billing"],"summary":"Get Subscription","description":"Get current subscription status.","operationId":"get_subscription_billing_subscription_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/billing/credits":{"get":{"tags":["billing","billing"],"summary":"Get Credits","description":"T7: Authoritative wallet balance — expiring and top-up split.\n\nReads directly from credit_grants; does not use users.credits_remaining cache.","operationId":"get_credits_billing_credits_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/billing/top-up/checkout":{"post":{"tags":["billing","billing"],"summary":"Create Top Up Checkout","description":"T4: Create a Stripe Checkout session for a top-up credit bundle.\n\nOnly registered users can purchase top-up credits.\nUses mode='payment' (one-time charge, not subscription).\nMetadata includes supabase_user_id and bundle_key for the webhook handler.","operationId":"create_top_up_checkout_billing_top_up_checkout_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopUpCheckoutRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopUpCheckoutResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/webhooks/stripe":{"post":{"tags":["webhooks","webhooks"],"summary":"Stripe Webhook","description":"Handle Stripe webhook events.","operationId":"stripe_webhook_webhooks_stripe_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/usage/history":{"get":{"tags":["usage","usage"],"summary":"Get Usage History","description":"Get processing history for authenticated user.\n\nReturns aggregated daily usage for the past N days.\n\nArgs:\n    days: Number of days to look back (1-90, default 30)\n\nReturns:\n    Aggregated usage data including total jobs, daily breakdown","operationId":"get_usage_history_usage_history_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Usage History Usage History Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/usage/summary":{"get":{"tags":["usage","usage"],"summary":"Get Usage Summary","description":"Get current usage summary (credits remaining, tier info).\n\nReturns:\n    User's current tier, credits, and subscription info","operationId":"get_usage_summary_usage_summary_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Usage Summary Usage Summary Get"}}}}}}},"/internal/cleanup":{"post":{"tags":["cleanup"],"summary":"Cleanup Expired Jobs","description":"Sweep one batch of expired jobs — called by Cloud Scheduler.\n\nThis path is in main.py's auth-middleware skip list, so the OIDC dependency\nabove is the only thing between the public internet and a bulk delete. It\nis not optional.\n\nReturns:\n    deleted:   rows successfully swept this run\n    errors:    per-job failure messages, if any\n    remaining: True when the batch came back full, meaning more work is\n               waiting for the next scheduled run","operationId":"cleanup_expired_jobs_internal_cleanup_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-cloudscheduler","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Cloudscheduler"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"options":{"tags":["cleanup"],"summary":"Cleanup Options","description":"Handle CORS preflight requests.","operationId":"cleanup_options_internal_cleanup_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/internal/refresh-daily-credits":{"post":{"tags":["internal","internal"],"summary":"Refresh Daily Credits","description":"Refresh daily credits for free tier users.\n\nCalled by Cloud Scheduler at midnight UTC.\n\nT3: Calls refresh_daily_grants RPC to emit credit_grants rows for registered\nfree-tier users (Phase 1 wallet). Also keeps the legacy guest session and\nusers.credits_remaining counters in sync (guests are unchanged in Phase 1).","operationId":"refresh_daily_credits_internal_refresh_daily_credits_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-cloudscheduler","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Cloudscheduler"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/slack/provision-session":{"post":{"tags":["internal","internal"],"summary":"Provision Slack Session","operationId":"provision_slack_session_internal_slack_provision_session_post","parameters":[{"name":"x-internal-token","in":"header","required":false,"schema":{"type":"string","default":"","title":"X-Internal-Token"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SlackProvisionRequest","default":{}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mcp/auth/device-code":{"post":{"tags":["mcp-auth","mcp-auth"],"summary":"Device Code Endpoint","description":"Generate a device code for MCP login. No auth required.\nRate limit: 10/hour per IP.","operationId":"device_code_endpoint_mcp_auth_device_code_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/mcp/auth/token":{"get":{"tags":["mcp-auth","mcp-auth"],"summary":"Token Endpoint","description":"Poll for mcp_token approval status. Called by MCP server during login.\nRate limit: 20/minute per device_code.","operationId":"token_endpoint_mcp_auth_token_get","parameters":[{"name":"device_code","in":"query","required":true,"schema":{"type":"string","title":"Device Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mcp/auth/approve":{"post":{"tags":["mcp-auth","mcp-auth"],"summary":"Approve Endpoint","description":"Called by web app when user clicks Approve. Requires authenticated user.\nRate limit: 10/hour per IP.","operationId":"approve_endpoint_mcp_auth_approve_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/mcp/auth/deny":{"post":{"tags":["mcp-auth","mcp-auth"],"summary":"Deny Endpoint","description":"Called by web app when user clicks Deny. Requires authenticated user.","operationId":"deny_endpoint_mcp_auth_deny_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/mcp/auth/revoke":{"post":{"tags":["mcp-auth","mcp-auth"],"summary":"Revoke Endpoint","description":"Revoke an MCP token (logout). Token sent in Authorization header.","operationId":"revoke_endpoint_mcp_auth_revoke_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"}}}}}}},"/mcp/auth/status":{"get":{"tags":["mcp-auth","mcp-auth"],"summary":"Status Endpoint","description":"Get account status. Accepts MCP token or session token.","operationId":"status_endpoint_mcp_auth_status_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-session-token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Session-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"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":{}}}}}},"post":{"tags":["api-keys"],"summary":"Create Key","operationId":"create_key_api_keys_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"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":"string","title":"Key Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api-keys/verify":{"get":{"tags":["api-keys"],"summary":"Verify Key","description":"Used by the WordPress plugin settings page to validate a key and show account status.","operationId":"verify_key_api_keys_verify_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/stats/recent":{"get":{"tags":["stats"],"summary":"Get Recent Stats","description":"Return anonymized recent completed jobs for Weight Wall social proof.\n\nFilenames are replaced with generic labels — original filenames are never exposed.","operationId":"get_recent_stats_stats_recent_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":5,"default":20,"title":"Limit"}},{"name":"tool","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"compress|resize|upscale|tag","title":"Tool"},"description":"compress|resize|upscale|tag"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecentStatsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"options":{"tags":["stats"],"summary":"Stats Recent Options","operationId":"stats_recent_options_stats_recent_options","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/":{"get":{"summary":"Root","description":"Root endpoint.","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"BatchStatusResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"jobs":{"items":{"$ref":"#/components/schemas/JobResponse"},"type":"array","title":"Jobs"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["success","jobs","total"],"title":"BatchStatusResponse","description":"Batch status response."},"Body_upload_file_upload_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File","description":"Image file to upload"}},"type":"object","required":["file"],"title":"Body_upload_file_upload_post"},"CheckoutRequest":{"properties":{"price_id":{"type":"string","title":"Price Id"},"success_url":{"type":"string","title":"Success Url"},"cancel_url":{"type":"string","title":"Cancel Url"}},"type":"object","required":["price_id","success_url","cancel_url"],"title":"CheckoutRequest"},"CheckoutResponse":{"properties":{"checkout_url":{"type":"string","title":"Checkout Url"},"session_id":{"type":"string","title":"Session Id"}},"type":"object","required":["checkout_url","session_id"],"title":"CheckoutResponse"},"CompressRequest":{"properties":{"temp_file_ids":{"items":{"type":"string"},"type":"array","title":"Temp File Ids"},"settings":{"anyOf":[{"$ref":"#/components/schemas/ProcessingSettings"},{"type":"null"}]},"output_format":{"type":"string","title":"Output Format","default":"original"},"quality":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Quality","default":80},"target_file_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Target File Size"}},"type":"object","required":["temp_file_ids"],"title":"CompressRequest","description":"Request model for compress endpoint with temp_file_ids."},"ConfirmUploadRequest":{"properties":{"temp_file_id":{"type":"string","title":"Temp File Id"},"filename":{"type":"string","title":"Filename"},"file_size":{"type":"integer","title":"File Size"},"mime_type":{"type":"string","title":"Mime Type"}},"type":"object","required":["temp_file_id","filename","file_size","mime_type"],"title":"ConfirmUploadRequest"},"CreateKeyRequest":{"properties":{"name":{"type":"string","maxLength":100,"minLength":1,"title":"Name"}},"type":"object","required":["name"],"title":"CreateKeyRequest"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"JobResponse":{"properties":{"id":{"type":"string","format":"uuid4","title":"Id"},"status":{"$ref":"#/components/schemas/JobStatus"},"shopify_store_id":{"anyOf":[{"type":"string","format":"uuid4"},{"type":"null"}],"title":"Shopify Store Id"},"original_filename":{"type":"string","title":"Original Filename"},"original_size":{"type":"integer","title":"Original Size"},"original_format":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Original Format"},"original_mime_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Original Mime Type"},"original_width":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Original Width"},"original_height":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Original Height"},"original_dpi":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Original Dpi"},"original_file_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Original File Path"},"output_format":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Output Format"},"output_upscale_factor":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Output Upscale Factor"},"output_width":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Output Width"},"output_height":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Output Height"},"output_resize_behavior":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Output Resize Behavior"},"output_quality":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Output Quality"},"output_file_size_limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Output File Size Limit"},"output_seo_tag_gen":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Output Seo Tag Gen"},"output_seo_rename":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Output Seo Rename"},"executed_upscale":{"type":"boolean","title":"Executed Upscale","default":false},"executed_resize":{"type":"boolean","title":"Executed Resize","default":false},"executed_compress":{"type":"boolean","title":"Executed Compress","default":false},"executed_tag":{"type":"boolean","title":"Executed Tag","default":false},"processed_file_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Processed File Path"},"processed_filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Processed Filename"},"processed_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Processed Size"},"processed_format":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Processed Format"},"processed_width":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Processed Width"},"processed_height":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Processed Height"},"processed_compression_ratio":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Processed Compression Ratio"},"seo_alt_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Seo Alt Text"},"seo_filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Seo Filename"},"seo_keywords":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Seo Keywords"},"job_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Job Source"},"job_entry_point":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Job Entry Point"},"processing_time_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Processing Time Ms"},"credits_used":{"type":"integer","title":"Credits Used","default":0},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"started_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Started At"},"completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Completed At"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"}},"type":"object","required":["id","status","original_filename","original_size","created_at","expires_at"],"title":"JobResponse","description":"Job response model matching database schema."},"JobStatus":{"type":"string","enum":["queued","processing","completed","failed","expired"],"title":"JobStatus","description":"Job status enum."},"OutputFormat":{"type":"string","enum":["original","jpg","png","webp","avif","gif","svg","ico"],"title":"OutputFormat","description":"Output format enum."},"PortalResponse":{"properties":{"portal_url":{"type":"string","title":"Portal Url"}},"type":"object","required":["portal_url"],"title":"PortalResponse"},"ProcessFilesRequest":{"properties":{"temp_file_ids":{"items":{"type":"string"},"type":"array","maxItems":50,"title":"Temp File Ids"},"settings":{"$ref":"#/components/schemas/ProcessingSettings"},"request_id":{"anyOf":[{"type":"string","format":"uuid4"},{"type":"null"}],"title":"Request Id","description":"Client-generated idempotency key (latency spec §Correlation contract). A retry with the same request_id and equivalent files+settings replays the original batch instead of creating and charging a second one. UUID4 shape-validates on input; the value is never trusted for billing or authorization on its own."}},"type":"object","required":["temp_file_ids"],"title":"ProcessFilesRequest","description":"Request to process previously uploaded temp files."},"ProcessFilesResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"jobs":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Jobs"},"credits_used":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Credits Used"},"credits_remaining":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Credits Remaining"}},"type":"object","required":["success","jobs"],"title":"ProcessFilesResponse","description":"Response for batch file processing."},"ProcessJobRequest":{"properties":{"job_id":{"type":"string","title":"Job Id"},"input_path":{"type":"string","title":"Input Path"},"settings":{"$ref":"#/components/schemas/ProcessingSettings"}},"type":"object","required":["job_id","input_path","settings"],"title":"ProcessJobRequest","description":"Request model for job processing."},"ProcessingSettings":{"properties":{"output_format":{"$ref":"#/components/schemas/OutputFormat","description":"Output format","default":"original"},"output_upscale_factor":{"anyOf":[{"type":"number","maximum":10.0,"minimum":0.1},{"type":"null"}],"title":"Output Upscale Factor","description":"Upscale factor (e.g., 2.0, 4.0)"},"auto_upscale":{"type":"boolean","title":"Auto Upscale","description":"Internal: upscale was auto-triggered, not user-requested","default":false},"output_width":{"anyOf":[{"type":"integer","maximum":8192.0,"minimum":1.0},{"type":"null"}],"title":"Output Width","description":"Target width in pixels"},"output_height":{"anyOf":[{"type":"integer","maximum":8192.0,"minimum":1.0},{"type":"null"}],"title":"Output Height","description":"Target height in pixels"},"output_resize_behavior":{"$ref":"#/components/schemas/ResizeBehavior","description":"How to handle aspect ratio mismatch: pad (default) or crop","default":"pad"},"output_quality":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Output Quality","description":"Compression quality 1-100","default":80},"output_file_size_limit":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Output File Size Limit","description":"Target max file size in bytes"},"output_seo_tag_gen":{"type":"boolean","title":"Output Seo Tag Gen","description":"Generate SEO metadata (alt text, keywords)","default":false},"output_seo_rename":{"type":"boolean","title":"Output Seo Rename","description":"Rename file with SEO-friendly name","default":false},"gif_frame_limit":{"anyOf":[{"type":"integer","maximum":100.0,"minimum":1.0},{"type":"null"}],"title":"Gif Frame Limit","description":"Max frames to process for animated GIFs (1-100). None means process all frames."}},"type":"object","title":"ProcessingSettings","description":"User's desired output settings for image processing.\n\nNote: No activation booleans (run_*) - components are determined by:\n- Upscale: if output_upscale_factor is set\n- Resize: if output_width or output_height is set\n- Compress: always runs (quality setting)\n- Tag: if output_seo_tag_gen is True"},"RecentJob":{"properties":{"filename":{"type":"string","title":"Filename"},"original_size":{"type":"integer","title":"Original Size"},"processed_size":{"type":"integer","title":"Processed Size"},"reduction_pct":{"type":"number","title":"Reduction Pct"}},"type":"object","required":["filename","original_size","processed_size","reduction_pct"],"title":"RecentJob"},"RecentStatsResponse":{"properties":{"jobs":{"items":{"$ref":"#/components/schemas/RecentJob"},"type":"array","title":"Jobs"}},"type":"object","required":["jobs"],"title":"RecentStatsResponse"},"ResizeBehavior":{"type":"string","enum":["pad","crop"],"title":"ResizeBehavior","description":"How to handle aspect ratio mismatch when both width and height are specified."},"ResizeRequest":{"properties":{"temp_file_ids":{"items":{"type":"string"},"type":"array","title":"Temp File Ids"},"settings":{"anyOf":[{"$ref":"#/components/schemas/ProcessingSettings"},{"type":"null"}]},"width":{"anyOf":[{"type":"integer","maximum":8192.0,"minimum":1.0},{"type":"null"}],"title":"Width"},"height":{"anyOf":[{"type":"integer","maximum":8192.0,"minimum":1.0},{"type":"null"}],"title":"Height"},"resize_behavior":{"type":"string","title":"Resize Behavior","default":"pad"},"output_format":{"type":"string","title":"Output Format","default":"original"}},"type":"object","required":["temp_file_ids"],"title":"ResizeRequest","description":"Request model for resize endpoint with temp_file_ids."},"SignedUploadUrlRequest":{"properties":{"filename":{"type":"string","title":"Filename"},"content_type":{"type":"string","title":"Content Type"},"file_size":{"type":"integer","title":"File Size"}},"type":"object","required":["filename","content_type","file_size"],"title":"SignedUploadUrlRequest"},"SignedUploadUrlResponse":{"properties":{"temp_file_id":{"type":"string","title":"Temp File Id"},"upload_url":{"type":"string","title":"Upload Url"},"gcs_path":{"type":"string","title":"Gcs Path"},"session_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Session Token"},"expires_in":{"type":"integer","title":"Expires In"}},"type":"object","required":["temp_file_id","upload_url","gcs_path","session_token","expires_in"],"title":"SignedUploadUrlResponse"},"SignedUrlResponse":{"properties":{"url":{"type":"string","title":"Url"},"filename":{"type":"string","title":"Filename"},"content_type":{"type":"string","title":"Content Type"},"size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Size"},"expires_in":{"type":"integer","title":"Expires In"}},"type":"object","required":["url","filename","content_type","expires_in"],"title":"SignedUrlResponse","description":"Response for signed URL endpoint."},"SlackProvisionRequest":{"properties":{"team_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Team Id"}},"type":"object","title":"SlackProvisionRequest"},"StatusResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"job":{"$ref":"#/components/schemas/JobResponse"}},"type":"object","required":["success","job"],"title":"StatusResponse","description":"Status check response."},"TagRequest":{"properties":{"temp_file_ids":{"items":{"type":"string"},"type":"array","title":"Temp File Ids"},"settings":{"anyOf":[{"$ref":"#/components/schemas/ProcessingSettings"},{"type":"null"}]},"rename_file":{"type":"boolean","title":"Rename File","default":false},"output_format":{"type":"string","title":"Output Format","default":"original"}},"type":"object","required":["temp_file_ids"],"title":"TagRequest","description":"Request model for tag endpoint with temp_file_ids."},"TempFileResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"temp_file_id":{"type":"string","title":"Temp File Id"},"original_filename":{"type":"string","title":"Original Filename"},"file_size":{"type":"integer","title":"File Size"},"mime_type":{"type":"string","title":"Mime Type"},"temp_path":{"type":"string","title":"Temp Path"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"session_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Session Token"},"gif_frame_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Gif Frame Count"},"gif_fps":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Gif Fps"}},"type":"object","required":["success","temp_file_id","original_filename","file_size","mime_type","temp_path","expires_at"],"title":"TempFileResponse","description":"Response for temp file upload (Phase 1 of two-phase upload)."},"TopUpCheckoutRequest":{"properties":{"bundle_key":{"type":"string","title":"Bundle Key"},"success_url":{"type":"string","title":"Success Url"},"cancel_url":{"type":"string","title":"Cancel Url"}},"type":"object","required":["bundle_key","success_url","cancel_url"],"title":"TopUpCheckoutRequest"},"TopUpCheckoutResponse":{"properties":{"checkout_url":{"type":"string","title":"Checkout Url"},"session_id":{"type":"string","title":"Session Id"},"bundle_key":{"type":"string","title":"Bundle Key"},"credits":{"type":"integer","title":"Credits"}},"type":"object","required":["checkout_url","session_id","bundle_key","credits"],"title":"TopUpCheckoutResponse"},"UpscaleRequest":{"properties":{"temp_file_ids":{"items":{"type":"string"},"type":"array","title":"Temp File Ids"},"settings":{"anyOf":[{"$ref":"#/components/schemas/ProcessingSettings"},{"type":"null"}]},"factor":{"type":"number","maximum":4.0,"minimum":1.0,"title":"Factor","default":2.0},"output_format":{"type":"string","title":"Output Format","default":"original"}},"type":"object","required":["temp_file_ids"],"title":"UpscaleRequest","description":"Request model for upscale endpoint with temp_file_ids."},"UrlUploadRequest":{"properties":{"url":{"type":"string","title":"Url"},"filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Filename"}},"type":"object","required":["url"],"title":"UrlUploadRequest","description":"Request body for URL-based upload."},"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"}}}}