{"openapi":"3.0.0","paths":{"/healthz":{"get":{"operationId":"HealthController_health","parameters":[],"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["status"],"summary":"Liveness probe. Returns ok when the service is up."}},"/api/v1/platform":{"get":{"operationId":"PlatformController_status_v1","parameters":[],"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["status"],"summary":"Public platform settings, such as whether virus scanning is on, plus the limits we actually enforce: max file size, mirrors per file, remote URLs per request, and how many uploads one account may have in flight at once."}},"/api/v1/upload-sessions":{"post":{"operationId":"UploadSessionController_create_v1","parameters":[],"responses":{"201":{"description":"data: sessionId and chunkSize. Send chunks of exactly this size.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The API key is valid but not allowed here: wrong scope (INSUFFICIENT_SCOPE) or an endpoint that API keys cannot use at all (API_KEY_NOT_ALLOWED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Conflict: the request clashes with existing state (duplicate name, quota, or chunk already accepted).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["upload"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUploadSessionInput"}}}},"security":[{"apiKey":[]}],"description":"Requires an API key with one of these scopes: upload:write.","summary":"Start a resumable, chunked upload session."}},"/api/v1/upload-sessions/{id}/chunks":{"patch":{"operationId":"UploadSessionController_appendChunk_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the upload session."},{"name":"upload-offset","in":"header","required":true,"description":"Byte offset of this chunk within the file (0-based).","schema":{"type":"string"}},{"name":"content-type","in":"header","required":true,"description":"application/offset+octet-stream","schema":{"type":"string"}}],"responses":{"200":{"description":"data: uploadedBytes and completed; once finished, fileId and the scan result. If the session carried a distribution request, `distribution` reports what happened: `{started:true, uploadJobId, taskCount, shareSlug, skipped[]}` or `{started:false, error}`. The upload itself still succeeded when `started` is false — check this field rather than assuming distribution began.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The API key is valid but not allowed here: wrong scope (INSUFFICIENT_SCOPE) or an endpoint that API keys cannot use at all (API_KEY_NOT_ALLOWED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found. Also returned when the resource exists but belongs to someone else — the two cases are deliberately indistinguishable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Conflict: the request clashes with existing state (duplicate name, quota, or chunk already accepted).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["upload"],"requestBody":{"required":true,"description":"Raw bytes of the chunk. NOT JSON.","content":{"application/offset+octet-stream":{"schema":{"type":"string","format":"binary"}}}},"security":[{"apiKey":[]}],"description":"Requires an API key with one of these scopes: upload:write.","summary":"Upload one chunk; the final chunk creates the distribution job."}},"/api/v1/upload-sessions/{id}":{"head":{"operationId":"UploadSessionController_head_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the upload session."}],"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The API key is valid but not allowed here: wrong scope (INSUFFICIENT_SCOPE) or an endpoint that API keys cannot use at all (API_KEY_NOT_ALLOWED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found. Also returned when the resource exists but belongs to someone else — the two cases are deliberately indistinguishable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["upload"],"security":[{"apiKey":[]}],"description":"Requires an API key with one of these scopes: upload:write.","summary":"Resume point. Returns Upload-Offset and Upload-Length headers, with no body (TUS semantics)."},"get":{"operationId":"UploadSessionController_status_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the upload session."}],"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The API key is valid but not allowed here: wrong scope (INSUFFICIENT_SCOPE) or an endpoint that API keys cannot use at all (API_KEY_NOT_ALLOWED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found. Also returned when the resource exists but belongs to someone else — the two cases are deliberately indistinguishable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["upload"],"security":[{"apiKey":[]}],"description":"Requires an API key with one of these scopes: upload:write, file:read.","summary":"Session progress: bytes received so far and whether it is complete."}},"/api/v1/mirror-tasks/start":{"post":{"operationId":"DistributionController_start_v1","parameters":[],"responses":{"202":{"description":"data: uploadJobId, taskCount, shareSlug and skipped[]. A provider that cannot accept this file (wrong type or too large) is skipped rather than failing the whole job — it appears in skipped[] with a reason. taskCount counts only the providers actually queued, so check skipped[] before assuming every host you asked for was used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The API key is valid but not allowed here: wrong scope (INSUFFICIENT_SCOPE) or an endpoint that API keys cannot use at all (API_KEY_NOT_ALLOWED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["mirror-tasks"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartDistributionInput"}}}},"security":[{"apiKey":[]}],"description":"Requires an API key with one of these scopes: upload:write.","summary":"Start distribution to the selected providers."}},"/api/v1/mirror-tasks/{uploadJobId}":{"get":{"operationId":"DistributionController_status_v1","parameters":[{"name":"uploadJobId","required":true,"in":"path","schema":{"type":"string"},"description":"Id returned when distribution was started."}],"responses":{"200":{"description":"data: job status plus tasks[] per provider (status, mirrorUrl, errorCode).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The API key is valid but not allowed here: wrong scope (INSUFFICIENT_SCOPE) or an endpoint that API keys cannot use at all (API_KEY_NOT_ALLOWED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found. Also returned when the resource exists but belongs to someone else — the two cases are deliberately indistinguishable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["mirror-tasks"],"security":[{"apiKey":[]}],"description":"Requires an API key with one of these scopes: upload:write, file:read.","summary":"Status of the job and of every mirror."}},"/api/v1/share/{slug}/unlock":{"post":{"operationId":"ShareController_unlock_v1","parameters":[{"name":"slug","required":true,"in":"path","schema":{"type":"string"},"description":"Share slug from the public share URL."}],"responses":{"200":{"description":"data: the same payload as the share endpoint, once the password checks out.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found. Also returned when the resource exists but belongs to someone else — the two cases are deliberately indistinguishable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["share"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnlockShareInput"}}}},"summary":"Unlock a password-protected share and get its links."}},"/api/v1/share/{slug}/view":{"post":{"operationId":"ShareController_view_v1","parameters":[{"name":"slug","required":true,"in":"path","schema":{"type":"string"},"description":"Share slug from the public share URL."}],"responses":{"204":{"description":"Success."},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found. Also returned when the resource exists but belongs to someone else — the two cases are deliberately indistinguishable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["share"],"summary":"Count one view of a share page."}},"/api/v1/share/{slug}":{"get":{"operationId":"ShareController_get_v1","parameters":[{"name":"slug","required":true,"in":"path","schema":{"type":"string"},"description":"Share slug from the public share URL. This is not a provider slug."}],"responses":{"200":{"description":"data: file name, size and the list of WORKING mirrors. If the share is password protected, data is only { slug, passwordRequired: true } — no file details are leaked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found. Also returned when the resource exists but belongs to someone else — the two cases are deliberately indistinguishable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["share"],"summary":"Public share page data. No authentication required."}},"/api/v1/remote-uploads":{"post":{"operationId":"RemoteUploadController_create_v1","parameters":[],"responses":{"200":{"description":"data.results[]: per URL, ok plus fileId or error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The API key is valid but not allowed here: wrong scope (INSUFFICIENT_SCOPE) or an endpoint that API keys cannot use at all (API_KEY_NOT_ALLOWED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["upload"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoteUploadInput"}}}},"security":[{"apiKey":[]}],"description":"Requires an API key with one of these scopes: upload:write.","summary":"Upload by fetching a remote URL (SSRF-protected)."}},"/api/v1/files":{"get":{"operationId":"FilesController_list_v1","parameters":[{"name":"q","required":true,"in":"query","schema":{"type":"string"},"description":"Filter by file name. Case-insensitive, matches anywhere in the name."},{"name":"status","required":true,"in":"query","schema":{"type":"string"},"description":"Filter by job state. \"active\" covers scanning, ready and distributing — all three mean \"not finished yet\". Omit for everything."},{"name":"sort","required":true,"in":"query","schema":{"type":"string"},"description":"Order of the results: newest (default), oldest, largest or name."},{"name":"cursor","in":"query","required":false,"description":"Id of the last item from the previous page. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"How many items to return.","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The API key is valid but not allowed here: wrong scope (INSUFFICIENT_SCOPE) or an endpoint that API keys cannot use at all (API_KEY_NOT_ALLOWED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["files"],"security":[{"apiKey":[]}],"description":"Requires an API key with one of these scopes: file:read.","summary":"List your files (cursor paginated). Newest first unless you pass sort, and filterable by name and job status."}},"/api/v1/files/{id}":{"get":{"operationId":"FilesController_detail_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the file."}],"responses":{"200":{"description":"data: file metadata, job summary with mirror counts, and share settings. Share password is never returned — only whether one is set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The API key is valid but not allowed here: wrong scope (INSUFFICIENT_SCOPE) or an endpoint that API keys cannot use at all (API_KEY_NOT_ALLOWED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found. Also returned when the resource exists but belongs to someone else — the two cases are deliberately indistinguishable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["files"],"security":[{"apiKey":[]}],"description":"Requires an API key with one of these scopes: file:read.","summary":"Details of one file: size, job status and share settings."},"delete":{"operationId":"FilesController_remove_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the file."},{"name":"keepMirrors","required":false,"in":"query","schema":{"type":"string"},"description":"Set to true to keep the copies held by providers and delete only the platform record. Defaults to false, which also deletes provider copies where deletion is supported."}],"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The API key is valid but not allowed here: wrong scope (INSUFFICIENT_SCOPE) or an endpoint that API keys cannot use at all (API_KEY_NOT_ALLOWED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found. Also returned when the resource exists but belongs to someone else — the two cases are deliberately indistinguishable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["files"],"security":[{"apiKey":[]}],"description":"Requires an API key with one of these scopes: file:delete.","summary":"Delete a file. Optionally also delete the copies held by providers."}},"/api/v1/files/{id}/share":{"patch":{"operationId":"FilesController_updateShare_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the file whose share page is being changed."}],"responses":{"200":{"description":"data: the updated share settings. The password itself is never returned — only whether one is set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The API key is valid but not allowed here: wrong scope (INSUFFICIENT_SCOPE) or an endpoint that API keys cannot use at all (API_KEY_NOT_ALLOWED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found. Also returned when the resource exists but belongs to someone else — the two cases are deliberately indistinguishable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["files"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateShareInput"}}}},"security":[{"apiKey":[]}],"description":"Requires an API key with one of these scopes: file:write.","summary":"Set share visibility, password and expiry."}},"/api/v1/files/{id}/mirrors":{"get":{"operationId":"FilesController_mirrors_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"},"description":"Id of the file."}],"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The API key is valid but not allowed here: wrong scope (INSUFFICIENT_SCOPE) or an endpoint that API keys cannot use at all (API_KEY_NOT_ALLOWED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found. Also returned when the resource exists but belongs to someone else — the two cases are deliberately indistinguishable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["files"],"security":[{"apiKey":[]}],"description":"Requires an API key with one of these scopes: file:read.","summary":"Mirrors of one file and the state of each."}},"/api/v1/providers/status":{"get":{"operationId":"ProvidersController_status_v1","parameters":[],"responses":{"200":{"description":"data.providers[]: successRate12h and successRate1h (percentages, null when nothing was attempted in the window), attempt counts, and the health-probe result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["providers"],"summary":"Public status page data: real upload success rate per provider, last 12 hours and last hour."}},"/api/v1/providers":{"get":{"operationId":"ProvidersController_list_v1","parameters":[],"responses":{"200":{"description":"data.providers: the provider catalogue (slug, name, status, allowedMime, maxFileBytes).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"400":{"description":"Invalid request (validation failure or provider rejection).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"tags":["providers"],"summary":"Provider catalogue with status, size and file-type limits."}}},"info":{"title":"MirrorUploadX API","description":"Upload a file once and mirror it to multiple third-party file hosts.\n\n## Authentication\n\nThis API is for server-to-server integrations and uses **one** credential:\nan **API key**, sent in the `x-api-key` header.\n\nKeys are scoped. A key only reaches the endpoints its scopes cover, and the\nscope needed is stated on every endpoint below:\n\n| Scope | Reaches |\n|---|---|\n| `upload:write` | creating upload sessions, sending chunks, remote fetches, starting distribution |\n| `file:read` | listing files, file detail, mirror status |\n| `file:write` | share settings on a file |\n| `file:delete` | deleting a file and its mirrors |\n\nA call with the wrong scope returns **403 `INSUFFICIENT_SCOPE`** and the\nmessage names the scope it needed.\n\nEndpoints not listed here — sign-in, account settings, API key management,\nwebhooks — belong to the web app's own session and cannot be reached with an\nAPI key at all. Create and manage your keys from the dashboard.\n\n## Uploading a file, end to end\n\nEvery field below is required and every name is exact. Copy this first, change\nit after.\n\n**1 — Open a session.** `sizeBytes` is mandatory; the server needs the exact\nbyte length up front to validate each chunk's offset.\n\n```bash\ncurl -X POST https://api.mirroruploadx.com/api/v1/upload-sessions \\\n  -H \"x-api-key: $KEY\" -H \"content-type: application/json\" \\\n  -d '{\"filename\":\"clip.mp4\",\"sizeBytes\":10485760,\"mimeType\":\"video/mp4\"}'\n```\n\nReturns `data.sessionId` and `data.chunkSize`. **The field is\n`sessionId`, not `id`** — reading the wrong one puts the string\n`undefined`/`None` into the next URL, which then 404s.\n\n**2 — Send the bytes.** One request per chunk, each exactly `chunkSize`\nexcept the last. The offset travels in a **header**, not the body or the\nquery string, and the content type is not `application/octet-stream`.\n\n```bash\ncurl -X PATCH https://api.mirroruploadx.com/api/v1/upload-sessions/$SESSION/chunks \\\n  -H \"x-api-key: $KEY\" \\\n  -H \"upload-offset: 0\" \\\n  -H \"content-type: application/offset+octet-stream\" \\\n  --data-binary @chunk-0.bin\n```\n\nEach reply carries `data.uploadedBytes` — use it as the next\n`upload-offset` rather than counting yourself. The **final** chunk also\nreturns `data.fileId`, and that is the only place you get it.\n\n**3 — Distribute.** Both fields are required.\n\n```bash\ncurl -X POST https://api.mirroruploadx.com/api/v1/mirror-tasks/start \\\n  -H \"x-api-key: $KEY\" -H \"content-type: application/json\" \\\n  -d '{\"fileId\":\"'\"$FILE\"'\",\"providerSlugs\":[\"gofile\",\"pixeldrain\"]}'\n```\n\nReturns `data.uploadJobId`, `data.shareSlug`, `data.taskCount` and\n`data.skipped[]`. **Read `skipped[]`** — a host that cannot take this file\nis skipped, not failed, so `taskCount` may be lower than the list you sent.\n\nYou can skip this step entirely by passing `distribution` when you open the\nsession in step 1; the server then starts distributing the moment the last\nchunk lands, with no second call and no need to keep your process alive.\n\n**4 — Watch it finish.** Poll `GET /api/v1/mirror-tasks/{uploadJobId}` every\nfew seconds until the job status is terminal. Do **not** poll\n`GET /api/v1/files/{id}` for this: mirrors are not files, and a fileId that\ndoes not exist yet returns 404 forever.\n\n### How many hosts at once\n\n`providerSlugs` is capped by the platform setting shown on your account\npage (`limits.maxMirrorsPerFile`, also returned by `GET /api/v1/platform`).\nGoing over returns **400 `TOO_MANY_MIRRORS`** and the message states the\nreal number. The catalogue is larger than that cap — being listed does not\nmean it fits in one distribution.\n\n## Rate limits\n\nLimits are counted **per API key**, not per IP address. Rotating IPs will not\ngive you more headroom, and two customers behind the same network never eat\ninto each other's budget.\n\nRequests without a key (the guest upload path) are counted per IP and get a\nsmaller budget: an anonymous caller has no account we can talk to if something\ngoes wrong.\n\nEvery response on a limited endpoint carries:\n\n| Header | Meaning |\n|---|---|\n| `X-RateLimit-Limit` | Requests allowed in the current window |\n| `X-RateLimit-Remaining` | How many you have left |\n| `X-RateLimit-Reset` | When the window rolls over (epoch seconds) |\n\nGoing over returns **429 `RATE_LIMITED`** with a `Retry-After` header.\nEndpoints that can return 429 say so in their response list below; the rest\nare not rate limited at all and do not send these headers — the public\ncatalogue, for example.\n\nChunk uploads are deliberately given a wide budget — a 10 GB file is roughly\n1280 chunks, and a limit that throttled real uploads would be an obstacle\nrather than a protection. The narrow limits sit where the cost is: opening a\nsession, and starting a distribution.\n\nThe exact numbers for your account, and how much of each you have used right\nnow, are shown on the **API keys** page in the dashboard.\n\n## Response envelope\n\nSuccessful responses:\n\n```json\n{ \"success\": true, \"data\": { } }\n```\n\nErrors use a **single shape**, alongside the HTTP status code:\n\n```json\n{ \"success\": false, \"error\": { \"code\": \"NO_TOKEN\", \"message\": \"…\", \"details\": [] } }\n```\n\n`code` is stable and meant to be read by machines; `message` is meant for\nhumans and may change. Handle errors **by code**, never by message text.\n\n### Common error codes\n\n| Code | HTTP | Meaning |\n|---|---|---|\n| `NO_TOKEN` / `UNAUTHORIZED` | 401 | Missing or invalid credentials |\n| `FORBIDDEN` | 403 | Insufficient permission (scope or ownership) |\n| `NOT_FOUND` | 404 | No such record |\n| `PROVIDER_REJECTS_FILE` | 400 | The chosen provider does not accept this file (type or size) |\n| `RATE_LIMITED` | 429 | Rate limit hit — see the `Retry-After` header |\n| `INTERNAL_ERROR` | 500 | Server error; details are logged, never returned |\n\n## Idempotency\n\nYou may send an `idempotencyKey` when creating an upload session. Repeating a\nrequest with the same key **does not create a second record** — it returns the\nexisting one. Use it to retry safely after a network failure.\n\nDistribution tasks are also created with a per-provider idempotent key, so\ntriggering the same job twice does not produce a duplicate upload.\n\n## Webhook signatures\n\nWebhook deliveries are signed with HMAC-SHA256 and sent in the\n`X-MUX-Signature` header. The signature covers the timestamp and the raw body.\nOn your side, compare in **constant time** and reject old timestamps to prevent\nreplay attacks.","version":"1","contact":{"name":"MirrorUploadX","url":"https://mirroruploadx.com","email":"support@mirroruploadx.com"},"termsOfService":"https://mirroruploadx.com/legal/terms","license":{"name":"MirrorUploadX Terms of Service","url":"https://mirroruploadx.com/legal/terms"}},"tags":[{"name":"status","description":"Service health and public platform settings"},{"name":"upload","description":"Upload sessions, chunk upload and remote URL upload"},{"name":"mirror-tasks","description":"Starting distribution and tracking job status"},{"name":"files","description":"Listing, inspecting and deleting files"},{"name":"providers","description":"Provider catalogue and per-user connections"},{"name":"share","description":"Public share page data"}],"servers":[{"url":"https://api.mirroruploadx.com","description":"Production"}],"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"x-api-key","description":"API key for server-to-server access."}},"schemas":{"CreateUploadSessionInput":{"type":"object","properties":{"filename":{"type":"string","minLength":1,"maxLength":255},"sizeBytes":{"type":"integer","exclusiveMinimum":0},"mimeType":{"type":"string","minLength":1,"maxLength":255},"idempotencyKey":{"type":"string","format":"uuid"},"distribution":{"type":"object","properties":{"providerSlugs":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":256},"minSuccess":{"type":"integer","minimum":0,"maximum":256},"expiresInDays":{"anyOf":[{"type":"integer","minimum":1,"maximum":365},{"type":"null"}]},"sharePassword":{"anyOf":[{"type":"string","minLength":4,"maxLength":128},{"type":"null"}]}},"required":["providerSlugs"],"additionalProperties":false}},"required":["filename","sizeBytes"],"additionalProperties":false},"ErrorEnvelope":{"type":"object","properties":{"success":{"type":"boolean","const":false},"error":{"type":"object","properties":{"code":{"type":"string","description":"Stable, machine-readable code. Branch on this, not on the message.","example":"NOT_FOUND"},"message":{"type":"string","description":"Human-readable explanation; may change."},"details":{"type":"object","additionalProperties":{},"description":"Optional extra context (for example which provider rejected the file and why)."},"requestId":{"type":"string"},"retryable":{"type":"boolean","default":false}},"required":["code","message","requestId"],"additionalProperties":false}},"required":["success","error"],"additionalProperties":false},"RemoteUploadInput":{"type":"object","properties":{"urls":{"type":"array","items":{"type":"string","format":"uri"},"minItems":1,"maxItems":50}},"required":["urls"],"additionalProperties":false},"StartDistributionInput":{"type":"object","properties":{"fileId":{"type":"string","format":"uuid"},"providerSlugs":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":256},"minSuccess":{"type":"integer","minimum":0,"maximum":256,"default":1},"expiresInDays":{"anyOf":[{"type":"integer","minimum":1,"maximum":365},{"type":"null"}]},"sharePassword":{"anyOf":[{"type":"string","minLength":4,"maxLength":128},{"type":"null"}]}},"required":["fileId","providerSlugs"],"additionalProperties":false},"UnlockShareInput":{"type":"object","properties":{"password":{"type":"string","minLength":1,"maxLength":128}},"required":["password"],"additionalProperties":false},"UpdateShareInput":{"type":"object","properties":{"visibility":{"type":"string","enum":["private","unlisted","public"]},"password":{"anyOf":[{"type":"string","minLength":4,"maxLength":128},{"type":"null"}]},"expiresAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]}},"additionalProperties":false},"ApiSuccess":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"description":"Endpoint-specific payload — see the response description."}}}}},"externalDocs":{"description":"Supported hosts and measured success rates","url":"https://mirroruploadx.com/status"}}