{
    "openapi": "3.1.0",
    "info": {
        "title": "Free AI Wiper customer API",
        "version": "1.0.0",
        "description": "Clean files on our servers through three steps: upload one file, poll its job, and download the verified result. The browser cleaner processes files locally; this API uploads them for server-side processing.\n\n## Access and credits\n\nA verified email address and a current Pro subscription are required. Free and Plus do not include this API. Sign in to [API keys](/account/api-keys), create a named key, and copy its secret once. Send `Authorization: Bearer YOUR_API_KEY` on all three endpoints. Website passwords and native-app OAuth tokens are not customer API keys. Keep keys on your server, out of frontend code, URLs and logs. Revoke unused keys in the account panel.\n\nPro includes 5,000 cleanups per monthly allowance period, including when Pro is billed yearly. The [plan and billing page](/account/plan) shows remaining credits and the next reset. Accepting a new job reserves one credit; successful processing consumes it even when `report.changed` is false. Terminal failure returns the reservation once. A matching idempotent retry never consumes another credit. Monthly allowance does not roll over. Purchased top-ups are used after the included allowance and remain usable while Pro access is active.\n\n## Quick start\n\nUse the same HTTPS origin as your portal: production is `https://freeaiwiper.com`; a test deployment may use a different origin. These shell examples require curl. Set the key privately in your environment and replace the example file and job ID.\n\n```sh\nBASE_URL='https://freeaiwiper.com'\n# Set FREEAIWIPER_API_KEY privately before running these commands.\nIDEMPOTENCY_KEY='image-clean-001'\ncurl --fail-with-body --silent --show-error \\\n  \"$BASE_URL/api/clean\" \\\n  -H \"Authorization: Bearer $FREEAIWIPER_API_KEY\" \\\n  -H 'Accept: application/json' \\\n  -H \"Idempotency-Key: $IDEMPOTENCY_KEY\" \\\n  -F 'file=@example.png;type=image/png' \\\n  -F 'personal_metadata=1'\n```\n\nKeep the same idempotency key when retrying this exact file and options. Generate a different key for each new operation. Do not manually set multipart `Content-Type`; curl supplies the boundary. Plain text uploads must omit `personal_metadata`.\n\nThe 202 response contains `data.id`, `data.status_url` and `data.download_url`. The two URLs are relative to `BASE_URL`. A 202 response confirms acceptance, not successful cleaning. Poll the status URL, initially every two seconds and then every five seconds while unchanged:\n\n```sh\nJOB_ID='80c69ef7-d674-4e8b-b793-c4153c11ce46' # Replace with data.id.\ncurl --fail-with-body --silent --show-error \\\n  \"$BASE_URL/api/jobs/$JOB_ID\" \\\n  -H \"Authorization: Bearer $FREEAIWIPER_API_KEY\" \\\n  -H 'Accept: application/json'\n```\n\n`queued` and `processing` mean wait. `ready` means the verified output is available. `failed` is an HTTP 200 job resource with a safe `data.error`; stop polling that job. `expired` means its result is no longer downloadable. Only download once status is `ready`:\n\n```sh\ncurl --fail --silent --show-error --location --proto-redir '=https' \\\n  \"$BASE_URL/api/jobs/$JOB_ID/download\" \\\n  -H \"Authorization: Bearer $FREEAIWIPER_API_KEY\" \\\n  --output cleaned.png\n```\n\nThe API returns a 302 redirect and storage returns the file. [Curl does not forward this Authorization header to a different host](https://curl.se/docs/manpage.html#-L); do not add `--location-trusted`. With other HTTP clients, disable forwarding of the API key to the storage host. Never log or share signed storage URLs. Compare downloaded bytes and SHA-256 with `data.output.bytes` and `data.output.sha256`; the sanitized attachment filename and MIME type describe the output format.\n\n## Limits, options and retention\n\nSupported formats: JPEG, PNG, WebP, AVIF, HEIC/HEIF, BMP, GIF, TIFF, PDF, DOCX, PPTX, XLSX, HTML, SVG, Markdown, MP4, MOV, WebM, MKV, WAV, MP3, FLAC, M4A and UTF-8 text. The format enum below gives their canonical IDs. Use a supported filename extension and matching MIME type; use `audio/mp4` for an M4A container. Internal structure is checked too. A .zip batch, remote URL or unsupported option is not an upload format.\n\nCanonical upload MIME types and accepted filename extensions are listed below. `application/octet-stream` is also accepted when the content and extension validate.\n\n| Format ID | Filename extensions | Canonical MIME type |\n| --- | --- | --- |\n| `jpeg` | `.jpg`, `.jpeg` | `image/jpeg` |\n| `png` | `.png` | `image/png` |\n| `webp` | `.webp` | `image/webp` |\n| `avif` | `.avif` | `image/avif` |\n| `heic` | `.heic`, `.heif` | `image/heic` |\n| `bmp` | `.bmp` | `image/bmp` |\n| `gif` | `.gif` | `image/gif` |\n| `tiff` | `.tif`, `.tiff` | `image/tiff` |\n| `pdf` | `.pdf` | `application/pdf` |\n| `docx` | `.docx` | `application/vnd.openxmlformats-officedocument.wordprocessingml.document` |\n| `pptx` | `.pptx` | `application/vnd.openxmlformats-officedocument.presentationml.presentation` |\n| `xlsx` | `.xlsx` | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` |\n| `html` | `.html`, `.htm` | `text/html` |\n| `svg` | `.svg` | `image/svg+xml` |\n| `markdown` | `.md`, `.markdown` | `text/markdown` |\n| `mp4` | `.mp4` | `video/mp4` |\n| `mov` | `.mov` | `video/quicktime` |\n| `webm` | `.webm` | `video/webm` |\n| `mkv` | `.mkv` | `video/x-matroska` |\n| `wav` | `.wav` | `audio/wav` |\n| `mp3` | `.mp3` | `audio/mpeg` |\n| `flac` | `.flac` | `audio/flac` |\n| `m4a` | `.m4a` | `audio/mp4` |\n| `text` | `.txt` | `text/plain` |\n\n`video/matroska` is also accepted for MKV.\n\nDefault limits are 25 MiB per image/PDF/Office file, 50 MiB per audio/video file, and 1 MiB per text/markup file. Deployments can configure a different media limit, up to 100 MiB, and stricter processor resource limits still apply. Default request limits are 30 uploads and 120 combined status/download requests per minute per account, shared across its keys. Parallel pollers share the same read allowance.\n\nAI metadata removal is always enabled for non-text formats. Personal metadata removal defaults to off; send `personal_metadata=1` to request it. Plain text uses the documented conservative trace defaults; custom trace lists and an `options` JSON field are not accepted. Unknown or unavailable detection is not proof of absence, and cleaning does not establish human authorship or remove every watermark.\n\nThe default result lifetime is one hour after readiness; always use `data.expires_at` as authoritative. Each download redirect issues a signed URL for at most five minutes and never beyond result expiry. Metadata and idempotency records are retained for seven days after success or terminal failure. Original uploads are scheduled for deletion after success or terminal failure; deletion can be retried if storage is temporarily unavailable. There is no customer cancellation or deletion endpoint. Revoking a key stops new API access but does not cancel accepted work or invalidate already issued storage URLs.\n\n## Errors and retries\n\nErrors contain `error.code`, a safe `error.message`, and `request_id`; keep the request ID for support. Proxy-generated errors may use a different envelope. Treat HTTP status and code together:\n\n- `401 unauthenticated`: use a valid, unrevoked customer API key.\n- `403 api_access_disabled`: verify the email address and current Pro access.\n- `404 job_not_found`: the job is absent, purged or belongs to another account.\n- `409 idempotency_conflict`: the key was used with different bytes, format or normalized options; do not blindly retry with changed data.\n- `409 job_not_ready` / `job_failed`: wait for readiness or inspect the failed job.\n- `410 result_expired`: submit a new operation with a new idempotency key if another result is needed.\n- `413 upload_too_large`, `415 unsupported_format`, `422 validation_failed`: fix the input before resubmitting. Processing may also reject a structurally unsupported file asynchronously.\n- `429 rate_limited`: wait for `Retry-After` before retrying. `429 api_quota_exhausted` on upload is different: a short backoff will not replenish credits; wait for the allowance reset or obtain a top-up.\n- `503 capacity_exhausted` / `service_unavailable`: back off, respect `Retry-After` when present, and retry the same upload with its original idempotency key.\n\nIf an upload response is lost, retry with the same key; never assume it was rejected. After the published record is purged, reusing an old key can create a new charged job. A network or infrastructure error is not permission to forward credentials to a different origin."
    },
    "servers": [
        {
            "url": "/",
            "description": "Same origin as the portal; production requires HTTPS."
        }
    ],
    "security": [
        {
            "CustomerApiKey": []
        }
    ],
    "tags": [
        {
            "name": "Cleaning jobs",
            "description": "Upload one file, poll its job and download the verified result."
        }
    ],
    "paths": {
        "/api/clean": {
            "post": {
                "tags": [
                    "Cleaning jobs"
                ],
                "operationId": "createCleaningJob",
                "summary": "Upload one file for cleaning",
                "description": "Accept exactly one file. Laravel detects its format and validates the upload and optional personal_metadata field;\nRust validates the internal structure during processing. Filename and declared\nMIME alone are insufficient. Remote URL imports and batch archives are unsupported.\nDefault limits: 25 MiB image/PDF/Office, 50 MiB audio/video, 1 MiB text/markup; deployments may configure a media cap up to 100 MiB;\nstricter processor limits still apply. Text inputs must be valid UTF-8.\nAmbiguous text/markup families use a supported extension plus content validation.\nRemove selected supported signals while preserving protected content;\nthis is not universal watermark removal. Some structurally valid PDFs with unsupported indirect stream-length layouts fail safely as unsupported_format. Some failures are discovered asynchronously.\nOriginal uploads are scheduled for deletion after success or terminal failure. Results expire one hour after readiness by default; use expires_at. One credit is reserved for a new job, consumed on success (including unchanged output), and returned on terminal failure. Matching retries return the same job, including terminal jobs.\n",
                "parameters": [
                    {
                        "name": "Idempotency-Key",
                        "in": "header",
                        "required": true,
                        "description": "Stable client-generated key for the file and normalized options, scoped to\nthe customer account across its keys. Identical retries return the existing\njob; different content/options return 409. Retained for seven days after\nterminal transition; reuse after metadata purge can create a new job.\n",
                        "schema": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 128,
                            "pattern": "^[A-Za-z0-9._:-]+$"
                        },
                        "example": "file-clean-001"
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "multipart/form-data": {
                            "schema": {
                                "type": "object",
                                "additionalProperties": false,
                                "required": [
                                    "file"
                                ],
                                "properties": {
                                    "file": {
                                        "type": "string",
                                        "format": "binary"
                                    },
                                    "personal_metadata": {
                                        "type": "integer",
                                        "enum": [
                                            0,
                                            1
                                        ],
                                        "default": 0,
                                        "description": "Optional multipart form field. Send 1 to also remove supported personal metadata, or omit/0 to retain it. Not applicable to plain text; omit for text files.",
                                        "example": 1
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "202": {
                        "description": "Durably accepted job, or matching retry in any state. Acceptance is not\na guarantee of cleaning success. No duplicate processing for a matching retry.\n",
                        "headers": {
                            "Location": {
                                "description": "Authenticated job status URL.",
                                "schema": {
                                    "type": "string",
                                    "format": "uri-reference"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/JobResponse"
                                },
                                "example": {
                                    "data": {
                                        "id": "80c69ef7-d674-4e8b-b793-c4153c11ce46",
                                        "status": "queued",
                                        "format": "png",
                                        "input_bytes": 4096,
                                        "options": {
                                            "ai_metadata": true,
                                            "personal_metadata": true
                                        },
                                        "created_at": "2026-10-20T12:00:00Z",
                                        "started_at": null,
                                        "completed_at": null,
                                        "expires_at": null,
                                        "status_url": "/api/jobs/80c69ef7-d674-4e8b-b793-c4153c11ce46",
                                        "download_url": "/api/jobs/80c69ef7-d674-4e8b-b793-c4153c11ce46/download",
                                        "output": null,
                                        "report": null,
                                        "error": null
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/AccessDisabled"
                    },
                    "409": {
                        "$ref": "#/components/responses/Conflict"
                    },
                    "413": {
                        "$ref": "#/components/responses/TooLarge"
                    },
                    "415": {
                        "$ref": "#/components/responses/Unsupported"
                    },
                    "422": {
                        "$ref": "#/components/responses/Invalid"
                    },
                    "429": {
                        "$ref": "#/components/responses/Throttled"
                    },
                    "503": {
                        "$ref": "#/components/responses/Unavailable"
                    }
                }
            }
        },
        "/api/jobs/{id}": {
            "get": {
                "tags": [
                    "Cleaning jobs"
                ],
                "operationId": "getCleaningJob",
                "summary": "Get job status and result details",
                "description": "Any active API key belonging to the job owner can access the job. Revoking\nthe submitting key does not cancel accepted work. Other-customer jobs return\nthe same 404 as nonexistent jobs. A processing failure is a 200 resource with\nstatus failed; it is not a failed HTTP status request. Start polling every two\nseconds, increasing to five while unchanged; respect Retry-After if present.\nMetadata is retained for seven days after readiness or terminal failure,\nthen purged; expired results can no longer be downloaded.\n",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/JobId"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Owned job state, safe report or safe processing failure.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/JobResponse"
                                },
                                "example": {
                                    "data": {
                                        "id": "80c69ef7-d674-4e8b-b793-c4153c11ce46",
                                        "status": "ready",
                                        "format": "png",
                                        "input_bytes": 4096,
                                        "options": {
                                            "ai_metadata": true,
                                            "personal_metadata": true
                                        },
                                        "created_at": "2026-10-20T12:00:00Z",
                                        "started_at": "2026-10-20T12:00:01Z",
                                        "completed_at": "2026-10-20T12:00:02Z",
                                        "expires_at": "2026-10-20T13:00:02Z",
                                        "status_url": "/api/jobs/80c69ef7-d674-4e8b-b793-c4153c11ce46",
                                        "download_url": "/api/jobs/80c69ef7-d674-4e8b-b793-c4153c11ce46/download",
                                        "output": {
                                            "bytes": 4000,
                                            "mime_type": "image/png",
                                            "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
                                        },
                                        "report": {
                                            "changed": true,
                                            "processor_version": "example-version",
                                            "verification": "passed",
                                            "removed": [
                                                {
                                                    "category": "ai_metadata",
                                                    "count": 1
                                                }
                                            ],
                                            "retained": [],
                                            "detection_status": "complete_for_supported_signals"
                                        },
                                        "error": null
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/AccessDisabled"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "429": {
                        "$ref": "#/components/responses/Throttled"
                    },
                    "503": {
                        "$ref": "#/components/responses/Unavailable"
                    }
                }
            }
        },
        "/api/jobs/{id}/download": {
            "get": {
                "tags": [
                    "Cleaning jobs"
                ],
                "operationId": "downloadCleaningJob",
                "summary": "Download a verified result through a storage redirect",
                "description": "Laravel authorizes access and redirects; storage transfers file bytes.\nConfigure clients to follow redirects without forwarding the API Authorization\nheader to storage. The signed URL is reusable bearer access, valid for at most\nfive minutes and never beyond result expiry. API-key revocation prevents new\nredirects but does not revoke an issued URL. Expiry may not terminate a transfer\nalready in progress. queued/processing return 409 job_not_ready, failed returns\n409 job_failed, and expired returns 410 result_expired.\n",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/JobId"
                    }
                ],
                "responses": {
                    "302": {
                        "description": "Follow Location for an attachment with the detected MIME and sanitized filename.",
                        "headers": {
                            "Location": {
                                "description": "Short-lived signed storage GET URL.",
                                "schema": {
                                    "type": "string",
                                    "format": "uri"
                                }
                            },
                            "Cache-Control": {
                                "schema": {
                                    "type": "string",
                                    "const": "no-store, private"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/AccessDisabled"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "409": {
                        "$ref": "#/components/responses/Conflict"
                    },
                    "410": {
                        "$ref": "#/components/responses/Expired"
                    },
                    "429": {
                        "$ref": "#/components/responses/Throttled"
                    },
                    "503": {
                        "$ref": "#/components/responses/Unavailable"
                    }
                }
            }
        }
    },
    "components": {
        "securitySchemes": {
            "CustomerApiKey": {
                "type": "http",
                "scheme": "bearer",
                "description": "Requires a verified email address and current Pro access. Create a named customer API key at /account/api-keys. Copy the secret once; only its hash is stored. Send Authorization: Bearer YOUR_API_KEY. Website passwords and native-app OAuth tokens are not customer API keys. Never put keys in frontend code, URLs or logs, or forward them to storage on download redirects."
            }
        },
        "parameters": {
            "JobId": {
                "name": "id",
                "in": "path",
                "required": true,
                "schema": {
                    "type": "string",
                    "format": "uuid"
                },
                "example": "80c69ef7-d674-4e8b-b793-c4153c11ce46"
            }
        },
        "schemas": {
            "CleaningOptions": {
                "type": "object",
                "additionalProperties": false,
                "description": "Read-only snapshot of server-selected processing settings returned with the job. Do not send this object in an upload. AI metadata removal is always enabled for non-text formats; personal metadata removal defaults to false. Plain text uses the eight conservative text trace defaults below, with no customer-selectable trace list.",
                "properties": {
                    "ai_metadata": {
                        "type": "boolean"
                    },
                    "personal_metadata": {
                        "type": "boolean"
                    },
                    "text_traces": {
                        "type": "array",
                        "minItems": 1,
                        "uniqueItems": true,
                        "items": {
                            "type": "string",
                            "enum": [
                                "zero_width_space",
                                "word_joiner",
                                "soft_hyphen",
                                "bidi_control",
                                "embedded_bom",
                                "unicode_tag",
                                "noncharacter",
                                "reserved_ignorable",
                                "deprecated_format",
                                "private_use",
                                "unusual_space"
                            ]
                        },
                        "default": [
                            "zero_width_space",
                            "word_joiner",
                            "soft_hyphen",
                            "embedded_bom",
                            "unicode_tag",
                            "noncharacter",
                            "reserved_ignorable",
                            "deprecated_format"
                        ]
                    }
                },
                "readOnly": true
            },
            "Format": {
                "type": "string",
                "description": "Stable catalogue ID; specific supported-content restrictions still apply.",
                "enum": [
                    "jpeg",
                    "png",
                    "webp",
                    "avif",
                    "heic",
                    "bmp",
                    "gif",
                    "tiff",
                    "pdf",
                    "docx",
                    "pptx",
                    "xlsx",
                    "html",
                    "svg",
                    "markdown",
                    "mp4",
                    "mov",
                    "webm",
                    "mkv",
                    "wav",
                    "mp3",
                    "flac",
                    "m4a",
                    "text"
                ]
            },
            "Job": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "id",
                    "status",
                    "format",
                    "input_bytes",
                    "options",
                    "created_at",
                    "started_at",
                    "completed_at",
                    "expires_at",
                    "status_url",
                    "download_url",
                    "output",
                    "report",
                    "error"
                ],
                "description": "One accepted file, one job across retries. The overall waiting/processing lifetime is\n45 minutes, with at most three worker attempts; there is no separate queue timeout.\nready requires report/output and result\nexpiry; failed requires error. queued/processing have null report/output/error.\nexpires_at is null until ready; completed_at records success or terminal failure.\nStatus becomes expired when the successful result expires, with report and output\nfacts retained until metadata purge. No worker IDs, attempt tokens, private object\nkeys, full filenames, extracted content or signed URLs appear in this resource.\n",
                "properties": {
                    "id": {
                        "type": "string",
                        "format": "uuid"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "queued",
                            "processing",
                            "ready",
                            "failed",
                            "expired"
                        ]
                    },
                    "format": {
                        "$ref": "#/components/schemas/Format"
                    },
                    "input_bytes": {
                        "type": "integer",
                        "minimum": 1
                    },
                    "options": {
                        "$ref": "#/components/schemas/CleaningOptions"
                    },
                    "created_at": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "started_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "completed_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "expires_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "status_url": {
                        "type": "string",
                        "format": "uri-reference"
                    },
                    "download_url": {
                        "type": "string",
                        "format": "uri-reference",
                        "description": "Authenticated API endpoint, never a signed storage URL; usable only when ready."
                    },
                    "output": {
                        "oneOf": [
                            {
                                "type": "null"
                            },
                            {
                                "type": "object",
                                "additionalProperties": false,
                                "required": [
                                    "bytes",
                                    "mime_type",
                                    "sha256"
                                ],
                                "properties": {
                                    "bytes": {
                                        "type": "integer",
                                        "minimum": 0
                                    },
                                    "mime_type": {
                                        "type": "string"
                                    },
                                    "sha256": {
                                        "type": "string",
                                        "pattern": "^[a-f0-9]{64}$"
                                    }
                                }
                            }
                        ]
                    },
                    "report": {
                        "oneOf": [
                            {
                                "$ref": "#/components/schemas/ProcessingReport"
                            },
                            {
                                "type": "null"
                            }
                        ]
                    },
                    "error": {
                        "oneOf": [
                            {
                                "$ref": "#/components/schemas/JobError"
                            },
                            {
                                "type": "null"
                            }
                        ]
                    }
                }
            },
            "ProcessingReport": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "changed",
                    "processor_version",
                    "verification",
                    "removed",
                    "retained",
                    "detection_status"
                ],
                "description": "Only for verified success. Findings are aggregate counts without extracted values.\nUnknown/unavailable detection is not absence. Text uses its existing cleaning\nguarantees and an actual-output preservation check, not a new library operation.\n",
                "properties": {
                    "changed": {
                        "type": "boolean"
                    },
                    "processor_version": {
                        "type": "string"
                    },
                    "verification": {
                        "type": "string",
                        "enum": [
                            "passed"
                        ]
                    },
                    "removed": {
                        "type": "array",
                        "maxItems": 3,
                        "items": {
                            "$ref": "#/components/schemas/FindingSummary"
                        }
                    },
                    "retained": {
                        "type": "array",
                        "maxItems": 3,
                        "items": {
                            "$ref": "#/components/schemas/FindingSummary"
                        }
                    },
                    "detection_status": {
                        "type": "string",
                        "enum": [
                            "complete_for_supported_signals",
                            "partially_unavailable"
                        ]
                    }
                }
            },
            "FindingSummary": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "category",
                    "count"
                ],
                "properties": {
                    "category": {
                        "type": "string",
                        "enum": [
                            "ai_metadata",
                            "personal_metadata",
                            "unicode_traces"
                        ]
                    },
                    "count": {
                        "type": "integer",
                        "minimum": 0
                    }
                }
            },
            "JobError": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "code",
                    "message"
                ],
                "properties": {
                    "code": {
                        "type": "string",
                        "enum": [
                            "malformed_file",
                            "unsupported_format",
                            "verification_failed",
                            "resource_limit",
                            "processing_timeout",
                            "storage_unavailable",
                            "processing_failed"
                        ]
                    },
                    "message": {
                        "type": "string"
                    }
                }
            },
            "JobResponse": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "data"
                ],
                "properties": {
                    "data": {
                        "$ref": "#/components/schemas/Job"
                    }
                }
            },
            "ErrorResponse": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "error",
                    "request_id"
                ],
                "description": "Safe API errors. Proxy-generated rejections may not use this envelope.",
                "properties": {
                    "error": {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                            "code",
                            "message"
                        ],
                        "properties": {
                            "code": {
                                "type": "string",
                                "enum": [
                                    "unauthenticated",
                                    "api_access_disabled",
                                    "api_quota_exhausted",
                                    "job_not_found",
                                    "idempotency_conflict",
                                    "job_not_ready",
                                    "job_failed",
                                    "result_expired",
                                    "upload_too_large",
                                    "unsupported_format",
                                    "validation_failed",
                                    "rate_limited",
                                    "capacity_exhausted",
                                    "service_unavailable"
                                ]
                            },
                            "message": {
                                "type": "string"
                            }
                        }
                    },
                    "request_id": {
                        "type": "string"
                    }
                }
            }
        },
        "responses": {
            "Unauthorized": {
                "description": "Missing, invalid or revoked key; code unauthenticated.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorResponse"
                        }
                    }
                }
            },
            "AccessDisabled": {
                "description": "A verified email address and current Pro subscription are required; code api_access_disabled. Free, Plus, expired or otherwise ineligible accounts cannot use the cleaning API.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorResponse"
                        }
                    }
                }
            },
            "NotFound": {
                "description": "Nonexistent, purged or other-owner job; code job_not_found.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorResponse"
                        }
                    }
                }
            },
            "Conflict": {
                "description": "idempotency_conflict on upload; job_not_ready or job_failed on download.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorResponse"
                        }
                    }
                }
            },
            "Expired": {
                "description": "Result expired; code result_expired.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorResponse"
                        }
                    }
                }
            },
            "TooLarge": {
                "description": "Category/application size limit exceeded; code upload_too_large.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorResponse"
                        }
                    }
                }
            },
            "Unsupported": {
                "description": "Unsupported or incompatible format; code unsupported_format.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorResponse"
                        }
                    }
                }
            },
            "Invalid": {
                "description": "Invalid upload, encoding, options or idempotency key; code validation_failed.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorResponse"
                        }
                    }
                }
            },
            "Throttled": {
                "description": "Request throttle: rate_limited. Default limits are 30 uploads and 120 combined status/download requests per minute per account across its keys; respect Retry-After when present. POST /api/clean can instead return api_quota_exhausted when included credits and top-ups are exhausted; wait for the monthly reset or obtain a top-up, rather than retrying after a short delay.",
                "headers": {
                    "Retry-After": {
                        "description": "Suggested delay in seconds.",
                        "schema": {
                            "type": "integer",
                            "minimum": 1
                        }
                    }
                },
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorResponse"
                        }
                    }
                }
            },
            "Unavailable": {
                "description": "Temporary infrastructure or capacity failure; code service_unavailable or capacity_exhausted.",
                "headers": {
                    "Retry-After": {
                        "description": "Suggested delay in seconds when known.",
                        "schema": {
                            "type": "integer",
                            "minimum": 1
                        }
                    }
                },
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorResponse"
                        }
                    }
                }
            }
        }
    }
}