Start with the validate-only endpoint POST /v1/uploads:validate; it checks sizeBytes, expectedHashes.sha256, archiveManifest, and buildMetadata without creating a session, storage object, or audit write. Creator automation then opens POST /v1/uploads, sends archive bytes directly to private Cloudflare R2 through a signed single PUT URL or signed multipart parts, refreshes parts with POST /v1/uploads/{upload_id}/parts, checks GET /v1/uploads/{upload_id}, completes with POST /v1/uploads/{upload_id}/complete, and cancels with POST /v1/uploads/{upload_id}/abort. The Next.js API receives metadata and lifecycle calls, never the archive body. Large multipart sessions remain active for 48 hours while each signed URL lasts 15 minutes. status.progress exposes uploadedBytes, percent, totalParts, and provider-confirmed completedParts with ETags, so Website, CLI, and GitHub Action clients resume only missing parts after a network interruption. Private staging objects cannot publish before complete, archive/content-type/size validation, and security processing; a bounded cleanup job aborts expired multipart uploads or deletes incomplete single objects. Complete returns an async job while scan, metadata extraction, indexing hooks, or final promotion continue; poll job.statusEndpoint. queued and processing are non-terminal, while succeeded, rejected, failed, and cancelled are terminal. job.progress is a truthful 0/1 completion unit rather than an estimated scan percentage. createdAt, updatedAt, startedAt, completedAt, retry attempts, nextAttemptAt, and the stable terminal reason distinguish active, retrying, and finished work without exposing provider prose. Canonical lifecycleStatus values are draft, uploading, uploaded, validating, scanning, quarantined, ready, published, failed, and cancelled; legacy status remains a compatibility alias. expectedSha256 remains a legacy alias, and sha256_mismatch rejects the upload before promotion. Keep Idempotency-Key stable for create and complete retries. Version request metadata supports version, changelog, file_category, game_slug, game_versions, loader_platforms, dependencies, requirements, and build_metadata; a repeated logical version returns duplicate_version. metadata-only edits do not require re-uploading bytes. The security worker atomically claims durable jobs, retries transient failures, and moves exhausted or unverified uploads to quarantine. ZIP policy returns archive_dangerous_entry, archive_encrypted_entry, archive_symlink_entry, archive_duplicate_path, or archive_path_traversal; nested archives are recursively inspected through depth three; prohibited executable scenarios are unsafe, while any leaf above the ClamAV stream limit requires manual review.
The isolated archive-security-worker inspects the immutable scan seal: full SHA-256, ZIP/7z/RAR structure, and the ClamAV verdict. Exact-SHA clean cache reuse also requires the same format, policy version, engine, and signature database. Transient failures receive at most three attempts; unknown verdicts, stale databases, and resource limits never become clean and leave the file private in quarantine/review.
Numeric upload defaults: single PUT up to 100 MiB; 16 MiB multipart parts at concurrency 4; one signing request contains at most 4 parts, bounding payload in flight to 64 MiB; one archive is capped at 20 GiB. The authenticated upload byte bucket is 24 GiB. A live R2 capacity probe verified a 64 MiB upload/download with matching SHA-256, RSS delta below 384 MiB, and mandatory cleanup; the 64 MiB × 4 profile was rejected for exceeding the memory budget.
When complete creates a non-terminal job with status queued or processing, the HTTP response is 202 Accepted and the Location header exactly equals job.statusEndpoint. If the job is already terminal (succeeded, rejected, failed, or cancelled), including an idempotent replay of that terminal result, complete returns 200 OK without repeating the side effect.
Replaying POST /v1/uploads with the same body and Idempotency-Key returns the original durable session with freshly signed upload instructions. Every single PUT URL has a maximum 900-second lease that the server row-lock checkpoints after signing and before response; an abort-winning checkpoint rejects the URL response. Send every returned header, including If-None-Match: *. A conditional 412 is an ambiguous existing write: do not overwrite staging; call the same complete endpoint, where HEAD, the immutable seal, and SHA-256 are authoritative. Signed URLs and provider upload identifiers are never stored in the generic idempotency record; after an ambiguous create timeout, inspect the upload-session before starting a new mutation.
For polling guidance, use retry.retryable, retry.strategy, retry.attempt, retry.maxAttempts, retry.nextAttemptAt, and retry.retryAfterSeconds. An HTTP response can expose the same minimum delay through Retry-After.
The website's manual mod form uses the same upload-session and private R2 backend. It shows transfer and security-check status, preserves a created mod draft after a network interruption, and reuses provider-confirmed multipart parts when the same file is retried. Before multipart commit, the server rechecks every exact part size and provider ETag/MD5; the async trust worker then streams the full object for SHA-256 verification before ready/publish. Settings > Developer shows recent sessions, scan failures, and quarantine/review status.