Home/Project/API

Moddingflow API

Sections

Mod Manager install and download

8

1. Search anonymously with ordinary curl: curl "https://moddingflow.com/v1/search?q=skyui&game=skyrim-se". 2. Read the selected artifact with GET https://moddingflow.com/v1/artifacts/{artifact_id} and confirm the local game/profile. 3. Resolve an install plan with POST https://moddingflow.com/v1/install-plans:resolve. 4. Only after confirmation, call POST https://moddingflow.com/v1/downloads/{artifact_id}/resolve. 5. Verify hashes.sha256 before installing. A public mod needs no account, API key, OAuth registration, Bearer token, special User-Agent, or X-Moddingflow header. URLs with or without a trailing slash are served directly without a 308. TypeScript, Python, and C# SDK methods work without accessToken. For account-scoped content that returns 401/403, complete OAuth and retry with a Bearer token. Retry 429, 5xx, expired signed URLs, and failed hashes with backoff and a fresh resolve response.

/403/profilehttps://moddingflow.com/v1/install-plans:resolvehttps://moddingflow.com/v1/artifacts/{artifact_id}https://moddingflow.com/v1/search?q=skyui&game=skyrim-se"https://moddingflow.com/v1/downloads/{artifact_id}/resolve

The website passes only the canonical moddingflow://download?v=1&artifact_id=<uuid> URI to the operating system. This is a neutral protocol that any Mod Manager can support, while the user chooses the default application. Accept only the exact v=1 version and a canonical artifact_id UUID. Reject unknown versions, extra or duplicate parameters, malformed UUIDs, HTTP URLs, signed URLs, OAuth codes, tokens, API keys, Website sessions, and every other secret. Never turn a failed handoff into a browser download.

After cold, warm, or second-instance activation, a strict parser places one normalized artifact_id in the activation inbox. The manager then calls GET /v1/artifacts/{artifact_id}, shows a local confirmation for the selected game, profile, and instance, resolves the plan through POST /v1/install-plans:resolve, and only after explicit confirmation calls POST /v1/downloads/{artifact_id}/resolve. Receiving the URI must not automatically start downloading or installing. The website records manager_handoff_attempt separately; a manager download is counted only after successful API download-job activation.

POST /v1/install-plans:resolveGET /v1/artifacts/{artifact_id}POST /v1/downloads/{artifact_id}/resolvev=1//download

Request the artifact, install plan, and download resolution anonymously first. If account-scoped content returns 401/403, start OAuth and retry with a Bearer access token. A native manager is a public client and uses Authorization Code + PKCE S256 in the system browser with an exact loopback redirect URI such as http://127.0.0.1:<port>/oauth/callback. Do not use a shared personal API key, client_secret, embedded WebView, Implicit Flow, or Password Grant, and never pass a code or token through moddingflow://. Store refresh tokens only in protected credential storage.

Fluxora continues to use the desktop_mod_manager public client. Every third-party manager registers its own public OAuth client and redirect URI and implements the strict protocol v1 parser, server-side artifact revalidation, local confirmation, and API download-job lifecycle. Never share one client_id, personal API key, or secret between managers. On Windows, a manager registers its own moddingflow capability, does not take over another default handler, and sends the user to the system app picker when the handler must change.

///403http://127.0.0.1:<port>/oauth/callback

Call GET https://moddingflow.com/v1/artifacts/{artifact_id} when an app handoff contains only a stable artifact UUID. The endpoint returns a private/no-store allowlist of game, mod, exact version, published clean artifact, SHA-256, size, permanent Moddingflow links, and the stable download resolve endpoint. It never creates or resumes a download job, never mints a grant, and never returns a signed URL, ETag, storage key, uploader email, or internal entitlement field. Public artifacts work anonymously; if a Bearer token is supplied it must carry mods:read and invalid credentials never fall back to anonymous access. Ask the user to confirm the local game/profile before calling download resolve.

/profile/no-storemods:readhttps://moddingflow.com/v1/artifacts/{artifact_id}

Use POST /v1/install-plans:resolve to turn selected mods, versions, or artifacts into a deterministic plan. Send game_slug, game_version, platform, loader, release_channel, and one or more stable selectors. The response adds typed dependency_constraints, conflicts, artifact_selection, decision_reasons, stable plan_id, and stable item_id values while retaining legacy dependency and step fields. Required and optional dependencies, conflicts, and embedded contents are machine-readable; embedded contents do not create a separate download, and optional artifacts require include_optional=true. New clients treat hashes.sha256 as canonical and may keep sha256 as a compatibility alias; both fields cover the complete immutable stored-object bytes and are checked after comparing size_bytes. Then call POST /v1/downloads/{artifact_id}/resolve for each artifact and install only after full-archive SHA-256 verification succeeds. Existing legacy public file URLs remain mapped to the same artifact identity during migration and do not require archive re-upload.

Manual safety review does not add a public v1 field or a new error code. An Under review artifact follows the normal resolve flow; an unsafe artifact fails closed through the already documented generic 404/unavailable contract and never receives a signed URL. Clients must not try to distinguish this case from other artifact unavailability.

A successful POST https://moddingflow.com/v1/downloads/{artifact_id}/resolve returns the same complete mod: PublicMod snapshot as the GET catalog, the exact selected version and downloaded artifact, plus distribution.service=moddingflow with permanent website_url, mod_url, and api_url values. The selected version can differ from mod.latest_release and clients must not replace it with the latest one. primary_url and expires_at describe a temporary direct signed URL; after expiry, repeat the stable download_grant.resolve_endpoint (also mirrored by resume.signed_url_expiry_refresh). Do not persist primary_url or substitute it for permanent links.web, source_url, or homepage_url.

POST /v1/install-plans:resolvePOST /v1/downloads/{artifact_id}/resolve/unavailableoptional=trueservice=moddingflowhttps://moddingflow.com/v1/downloads/{artifact_id}/resolve

When resolve returns artifact_source=external_provider_reference, treat transport as a discriminated union rather than a managed CDN URL. transport.kind is external_provider_reference and carries provider, reference_id, reference_revision, a short-lived url, expires_at, and ttl_seconds; the response also includes exact size_bytes, hashes.sha256, and truthful capabilities. The URL is not identity, must not be persisted or logged, and is refreshed by calling the stable POST /v1/downloads/{artifact_id}/resolve again.

Follow head_supported and range_supported. When conditional_requests=false, conditional_headers is empty, ETag/if_match are null, and the client must not invent If-Match. Resume only when range_supported=true and SHA-256 is unchanged, then compare size_bytes and the full file's SHA-256 after assembly. For external transport, fallback.available=false and fallback endpoints and URLs are null: do not call Bunny/R2 fallback, open a browser, or start a separate direct-HTTP path from manager mode. Source-hosted bytes are not counted as Moddingflow egress.

Resolve is available only for an active, verified reference with a fresh server attestation. reference_pending and reference_stale mean wait for validation, reference_withdrawn and hash_mismatch mean stop installation, and provider_rate_limited or provider_unavailable may be retried only according to retry guidance. Removal or changed bytes block the old version; a changed file requires a new version. artifact_source=rehosted_blob keeps the existing managed session, HEAD, Range, and fallback semantics unchanged.

POST /v1/downloads/{artifact_id}/resolve/R2/if_matchrequests=falsesupported=trueavailable=falsesource=rehosted_blobsource=external_provider_reference

For a managed artifact_source=rehosted_blob, download resolve creates or resumes an actor-bound durable download job with one stable short-lived grant and returns 200 JSON after successful grant activation; it does not use a 302 redirect or stream binary data. Persist artifact_id, download_job.id, download_job.grant_id, and download_session_id, not primary_url, head_url, or fallback_url. primary_url is for GET with Range and If-Match/If-None-Match/If-Modified-Since/If-Unmodified-Since; the separately signed head_url is for HEAD with Content-Length and ETag. Use expires_at, expires_in, and refresh_after_seconds to call POST /v1/downloads/{artifact_id}/resolve again, confirm the same hashes.sha256/sha256 and size_bytes, and continue from the last durable byte after a URL expires. Fallback uses POST /v1/downloads/{artifact_id}/fallback with downloadSessionId and only mints another signed URL while the grant is active.

hashes.sha256 and the legacy sha256 alias are the application-level SHA-256 of the complete immutable stored-object bytes. After ordinary HTTP decoding, reassemble the complete object from every Range part in byte order, compare size_bytes, and only then verify the digest; one 206 response is not independently verified by this full-file hash. ETag is not a cryptographic digest: it is an opaque validator for one transport representation. Read representation.provider, etag_scope, etag, and if_match, and send If-Match only when representation.provider equals etag_scope. After provider failover, fallback intentionally returns etag=null, if_match=null, and requires_head_before_range=true: HEAD the fallback representation first and use only that provider's new ETag. Signed transports must preserve the stored bytes without transcoding; the current contract does not guarantee RFC 9530 Content-Digest or Repr-Digest, so clients rely on the response-body hash fields.

Before URL signing, the access tier is checked fail-closed: public allows anonymous download, authenticated requires an actor, paid requires an active subscriber entitlement, and restricted requires an active artifact/mod resource grant; an expired or revoked entitlement returns 403 without a signed URL. Access is also limited by client/app identity, files:download or delegated download scope, route rate limits, and byte budgets. Treat 404 as unavailable artifact, 401/403 as auth or entitlement problems, 429 as backoff, and 410 or download_session_expired as an expired or unusable grant. On 429, honor Retry-After, persist the safe download_job from Problem Details, and retry with job_id plus monotonic progress.bytes_received, progress.provider, and the matching ETag; repeated limits preserve the same job and grant and do not repeat provider work. Signed URL hosts, provider names, and query strings are opaque transport details and may change without breaking stable clients.

POST /v1/downloads/{artifact_id}/resolvePOST /v1/downloads/{artifact_id}/fallback/mod/app/403/sha256etag=nullmatch=nullrange=truefiles:downloadsource=rehosted_blob/If-None-Match/If-Modified-Since/If-Unmodified-Since

Treat 400 and 422 as request bugs, 401 or 403 as auth/scope problems, 404 or 410 as stale selections, 409 as a conflict to re-plan, 429 as backoff, and 5xx as retryable with jitter. Errors use RFC 9457 Problem Details with application/problem+json: branch on type or code, never localized detail. Every runtime error includes retryable and docs_slug. retry_after_seconds and quota_bucket appear only when the server has safe retry or quota guidance, required_scopes only when those scopes can make the authorization pass, and fields only for safe validation details. The OpenAPI x-moddingflow-error-registry is the source of truth and is additive within v1; preserve unknown future codes and fall back to numeric status. Typed SDK errors keep these fields in the parsed payload together with headers, request_id, and trace_id. On 429, wait at least Retry-After seconds and read RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset before jittered exponential backoff. Keep one Idempotency-Key per logical write or deterministic resolve retry; 409 idempotency_conflict means reuse the original body or start a new operation with a new key. Always keep X-Request-Id, request_id, and trace_id with support logs.

For external references, provider_unsupported (400) and locator_invalid (422) require a corrected request; source_already_imported (409) means reuse the existing reference. reference_pending and reference_stale (409) block resolve until server validation succeeds, reference_withdrawn (410) and hash_mismatch prohibit installation, and fallback_unavailable (409) confirms that no managed mirror exists. provider_rate_limited (429) follows Retry-After and provider_unavailable (503) uses backoff. Never work around these errors with a stored provider URL; obtain a fresh resolve only after the reference is verified and fresh again.

/scope/problemIdempotency-Key

Creator upload and publish automation

7

The primary Upload API target is a creator who builds a mod archive locally or in CI, then publishes a new version for an existing mod without opening the browser form. The same path must fit GitHub release or tag pipelines: build the archive, run local checks, create or reuse the version draft, upload direct-to-storage, wait for scan and validation, then publish only after the API reports ready.

Team publishing uses individual API keys, OAuth apps, or PATs with the smallest available scopes, so maintainers never share the owner password or main account session. Tool integrations should be able to place a Publish to Moddingflow button in a launcher, editor, or build tool and still show the same machine-readable upload status, request_id, and trace_id that CI sees.

Failure states are part of the product contract: upload_interrupted keeps the upload resumable or abortable, scan_failed and validation_failed block publish with details, permission_denied names the missing scope or project access, quota_exceeded returns retry guidance, and duplicate_version asks the client to reuse or change the version. Suspicious files move to quarantine/review; clients receive a clear status instead of a silent failure. The public promise is safer automation than the manual form: direct cloud upload, least-privilege keys, no shared owner credentials, auditable provenance, and security checks before a file reaches users.

/review

When rollout is enabled, register an external file with POST /v1/mods/{mod_id}/versions/{version_id}/external-artifact and a required Idempotency-Key. Send a discriminated provider locator, expected_size_bytes, expected hashes, content_type, canonical evidence, and provenance, never an arbitrary download URL. A successful 202 returns artifact_id, reference_id, validation_job_id, state=pending, and status_resource; poll GET /v1/external-artifact-validation-jobs/{validation_job_id}. The same key and body return the original result, while the same key with a different body returns 409 idempotency_conflict.

The current registry accepts only canonical GitHub, Modrinth, Hangar, and Codeberg locators. An arbitrary provider returns provider_unsupported and an unstable locator returns locator_invalid. A provider URL or signed URL is never identity and is never stored in the database or logs. Provider-locator uniqueness prevents duplicate imports; hashes are used for the server scan cache but do not replace identity. Modrinth requires SHA-512, SHA-1, and a locally calculated SHA-256; every provider requires exact size and SHA-256.

files:reference is a sensitive review_required scope: granting it requires AAL2, explicit consent, an approved client or credential path, and an owner, maintainer, or publisher role on the mod. Registration is restricted to public, free mods. Independent registration_enabled, worker_enabled, and resolve_enabled flags default to false. Four provider gates — github_enabled, modrinth_enabled, hangar_enabled, and codeberg_enabled — are also independent and default off; the selected provider must be enabled for registration, worker claim, revalidation, manager view, and resolve. Successful registration does not imply resolve is enabled. The worker is authoritative: it fetches provider metadata and bytes, verifies hash, size, archive, and security policy, and writes an immutable attestation; importer evidence remains advisory.

GET /v1/external-artifact-validation-jobs/{validation_job_id}POST /v1/mods/{mod_id}/versions/{version_id}/external-artifactstate=pendingIdempotency-Keyfiles:reference

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.

POST /v1/uploadsPOST /v1/uploads:validateGET /v1/uploads/{upload_id}POST /v1/uploads/{upload_id}/partsPOST /v1/uploads/{upload_id}/abortPOST /v1/uploads/{upload_id}/complete/1/MD5/7z/RAR/review/publish/downloadIdempotency-Key/content-type/size

Choose the manual browser form for a guided occasional release. Choose Upload API for repeatable local tools, CI, and GitHub Actions. Both use the same upload-session backend, private Cloudflare R2 storage, validation, scan, quarantine/review, and ready-before-publish security gates.

Give every person or automation an individual resource-scoped key with the smallest required scopes. The raw secret is one-time. Store MODDINGFLOW_API_KEY in a GitHub environment or repository secret, never in workflow YAML, repository variables, logs, command arguments, or artifacts. Prefer protected environments, minimal workflow permissions, and a reviewed 40-character Action commit SHA.

After scan_failed or av_scan_rejected, do not publish or repackage to bypass the verdict. Keep the file private, poll GET /v1/uploads/{upload_id}, record supportCorrelationId or X-Upload-Correlation-Id, request_id, trace_id, and the error code, then follow quarantine/review, manual review, or support guidance. The correlation id is safe to share with support: it contains no secret and grants no access. Create a new session only after fixing the identified cause.

Open the FAQ for workflow decisions and the API reference for status, error, and scope schemas.

GET /v1/uploads/{upload_id}/review

Game, Mod, ModVersion, Artifact, Blob, UploadSession, DownloadGrant, User, Team, Dependency, Job, WebhookSubscription, and WebhookDelivery each have a separate opaque UUID, ownership rule, and fail-closed state machine. A slug, username, team name, filename, URL, storage key, ETag, or hash is not public identity. Persist the returned UUID; rename never changes it.

Archive is reversible, but permanent deletion creates a retained tombstone. An unknown UUID returns 404; a tombstoned UUID returns 410 and is never reused. The canonical version lifecycle is draft → validating → scanning → ready → published → archived/withdrawn, with failed as an explicit outcome. uploaded, quarantined, approved, and rejected remain compatibility aliases for old clients.

On first publication the server captures an immutable snapshot of version, file kind, release channel, game and loader bindings, Blob UUID, size, and content type plus its SHA-256. Those fields cannot change in place after publication; archived/withdrawn restore is accepted only when the snapshot still matches exactly and the current upload-trust gates pass again. Public concurrent Mod/ModVersion PATCH uses a strong ETag in If-Match: a missing validator returns 428, while a stale validator returns 412 and the current ETag. lifecycle_version remains the internal optimistic lock for lifecycle transitions.

/withdrawn/ModVersion

Contract 1.1 mod-media additively introduces PublicMod.cover, PublicMod.gallery, and PublicModMedia. GET /v1/mods/{mod_id}/media returns project-owned media. POST /v1/mods/{mod_id}/media accepts a multipart file and JSON metadata; POST /v1/mods/{mod_id}/media:fallback creates a deterministic cover; DELETE /v1/mods/{mod_id}/media/{media_id} removes importer-managed media only. Writes reuse mods:write with Idempotency-Key and a strong If-Match. Internal rights evidence is excluded from public responses, and publishing without a cover returns cover_required.

GET /v1/mods/{mod_id}/mediaPOST /v1/mods/{mod_id}/mediaPOST /v1/mods/{mod_id}/media:fallbackDELETE /v1/mods/{mod_id}/media/{media_id}mods:writeIdempotency-Key

Publishing automation creates or updates a mod draft, creates a version draft, attaches a clean upload artifact, validates metadata, then calls the publish endpoint only when the creator is ready. Use mods:write for mod metadata and versions:write for version changes.

mods:writeversions:write

Catalog and search integrations

3

Use GET /v1/games, GET /v1/categories, GET /v1/mods, GET /v1/mods/{mod_id}, GET /v1/mods/{mod_id}/versions, GET /v1/mods/{mod_id}/dependencies, and GET /v1/search for catalog integrations. List and search use cursor-first keyset pagination with cursor, limit (default 25, max 100), stable tie-breaking, and opaque v1 base64url cursor values; echo the cursor from the previous response and do not generate offset or page-number cursors. Each cursor is bound to the resource, normalized filters, sort, and first-page insertion snapshot. Reusing it with changed context returns 400 invalid_input, and rows created or published after page one remain outside that cursor's snapshot. GET /v1/mods and GET /v1/search accept game_slug, q, sort=relevance|latest|trending|downloads, game_version, loader, and category filters; the website compatibility namespace also accepts game/gameSlug, query/search, gameVersion/version, platform, and facet aliases. Search requires q, defaults sort to relevance, and uses the same bounded RU/EN/DE query plan with normalization, aliases, keyboard correction, transliteration and typo recovery as site search and Agent Gateway mods.search. Its response schema, pagination and cursor contract are unchanged. List defaults to latest when no query is provided. Reserved resource filters are versions: mod_id, release_channel, game_version, loader, published_after, published_before; files: mod_id, version_id, artifact_id, file_kind, status, sha256; upload sessions: status, strategy, file_id, created_after, created_before; download grants: artifact_id, status, expires_after, expires_before; webhook deliveries: event_kind, delivery_status, created_after, next_attempt_before.

Anonymous catalog GET responses use Cache-Control public, a weak ETag, and Vary: Accept-Encoding, Authorization, Cookie. Send If-None-Match with the stored ETag to receive an empty 304 when the representation is unchanged. A request carrying Authorization or Cookie is always private no-store and does not reuse the public validator, so authenticated and anonymous responses cannot share a cache entry.

Example full-metadata flow: GET https://moddingflow.com/v1/mods?game_slug=skyrim-se-ae&limit=25, then GET the absolute links.api URL. PublicMod keeps summary as a short excerpt while description returns the untruncated Markdown from the first visible post with description_format=markdown. author is only the Moddingflow uploader, and profile_url is null for a closed profile. latest_version remains the compatibility field and is derived from the first active main file; latest_release contains the authoritative id, the same file version, primary_artifact_id, first-publication published_at, and release updated_at, or null for a legacy record without a confirmed version. links.web is the permanent Moddingflow page and links.api/links.versions are permanent API URLs. homepage_url belongs to the author or project, while source_url remains the uploader-supplied upstream page and is never replaced by a Moddingflow URL.

GET /v1/modsGET /v1/gamesGET /v1/searchGET /v1/categoriesGET /v1/mods/{mod_id}GET /v1/mods/{mod_id}/versionsGET /v1/mods/{mod_id}/dependencies/EN/DE/search/version/gameSlugsort=relevanceformat=markdown/links.versionshttps://moddingflow.com/v1/mods?game_slug=skyrim-se-ae&limit=25

Public payloads use stable UUIDs for games, mods, versions, artifacts, and dependencies. Do not depend on website forum topic IDs, storage object names, or human-readable slugs as durable integration keys.

Renames, metadata rebuilds, and slug changes keep existing mod, version, artifact, upload-session, and download-grant identifiers. Replacing file bytes creates a new physical blob and may create a new logical artifact/version binding; metadata-only edits do not force clients to re-upload files.

/version

The stable v1 model separates mod, version, logical file, physical blob, upload-session, download-grant, token, and webhook-delivery. A mod owns versions, a version points to logical files through artifact ids, and a logical file may bind to an immutable physical blob in storage without exposing storage paths.

Treat slug and localized slugs as mutable aliases, filename as display/download text, and hash values such as sha256 or hashes.sha256 as integrity checks. Persist public ids and lifecycle ids instead: mod id, version id, artifact id, sessionId, download_session_id, token metadata id, and webhook delivery id when webhook delivery endpoints are published.

The PublicModVersion.artifact_ids field remains the stable linkage. Optional PublicModVersion.artifacts appears only for a managed primary artifact when size_bytes > 0, sha256/hashes.sha256 are exact, the current upload session matches both file_id and final_blob_id, actual AV scan_status=clean, lifecycle=ready, publication state=published, and managed resolve is eligible; otherwise the expansion is omitted. The complete object contains id, mod_id, version_id, file_kind, file_version for the exact file, a non-null label, nullable original_filename, content_type, game_version_key, and loader_key, then size_bytes, sha256, hashes, artifact_source, status, scan_status, and download_metadata. PublicModVersion.version equals artifact.file_version; PublicMod.latest_version and latest_release.version reflect the first active main-file version for compatibility. Actual AV states are pending, queued, scanning, clean, quarantined, rejected, failed, and skipped. download_metadata contains a relative resolve_endpoint and truthful range_supported value, never a provider URL.

/downloadstatus=clean/hashes.sha256lifecycle=readystate=published

Agent Gateway delegated tools

3

The official desktop-only POST /api/fluxora/ai/agent route is not part of the public /v1 Agent Gateway. It requires OAuth client desktop_mod_manager with agent:run and X-Fluxora-AI-Protocol: 4, fixes provider/model to OpenAI GPT-5.6 Luna, and accepts only responses.create, responses.input_tokens, or models.retrieve. Third-party clients cannot register for agent:* scopes or use this route as an OpenAI proxy. After v4 activation, the old Gemini/v3 route returns 410 ai_client_upgrade_required.

POST /api/fluxora/ai/agent/v1/v3/modelagent:*agent:run

Agent Gateway exposes narrow tools for mods.search, mods.get, install_plans.resolve, downloads.resolve, versions.compare, publisher.draft, and publisher.validate. Agents should call tools instead of scraping pages or fetching arbitrary URLs.

An agent receives a short-lived token with audience agent-gateway and narrow tool scopes. It never receives the full user session token. Read tools can run directly; write-capable tools stay dry-run unless a confirmation token authorizes the specific action.

Auth and URL boundaries

2

API base URL: https://moddingflow.com/v1. OAuth issuer URL: https://moddingflow.com. API compatibility namespace: https://moddingflow.com/api/v1. Both namespaces accept operation URLs with or without a trailing slash directly, without a 308. The official CLI also provides a local sandbox profile at http://127.0.0.1:3000/api/v1; protected staging uses the operator-provided MODDINGFLOW_STAGING_API_BASE_URL and a staging-only key, not an anonymous shared credential.

Use the API base URL for /v1 resources. Use the OAuth issuer URL for /oauth/authorize, /oauth/token, /oauth/device_authorization, /oauth/revoke, /oauth/introspect, and RFC 8414 metadata at /.well-known/oauth-authorization-server. OIDC clients use /.well-known/openid-configuration and /.well-known/jwks.json; a 503 means that deployment is OAuth-only and must not receive the openid scope. The canonical transport remains the apex https://moddingflow.com/v1; /api/v1 is the compatible alias. No dedicated API host is required.

Stable v1 uses REST/JSON over HTTPS. Read endpoints stay GET resource routes, including GET /v1/uploads/{upload_id} for upload-session status, while write endpoints stay POST/PATCH mutations and lifecycle actions use explicit action routes such as /v1/uploads/{upload_id}/complete, /v1/mods/{mod_id}/versions/{version_id}/publish, and /v1/install-plans:resolve. Public clients address files with artifact_id, upload_id, and download_session_id, never storage object keys, provider bucket names, or signed URL secrets.

GET /v1/uploads/{upload_id}/v1/JSON/PATCH/api/v1/oauth/token/oauth/revoke/oauth/authorize/oauth/introspect/.well-known/jwks.jsonhttps://moddingflow.com/v1/install-plans:resolvehttps://moddingflow.com/v1/oauth/device_authorizationhttp://127.0.0.1:3000/api/v1https://moddingflow.com/api/v1/v1/uploads/{upload_id}/complete/.well-known/openid-configuration/.well-known/oauth-authorization-server/v1/mods/{mod_id}/versions/{version_id}/publish

Integrate native, desktop, mobile, CLI, local and server apps with PKCE S256, Device Flow, secretless Dynamic Client Registration or account-managed confidential credentials. OAuth

Runnable examples

3

Open the OpenAPI JSON, configure apiBaseUrl https://moddingflow.com/v1 and oauthIssuerUrl https://moddingflow.com, then choose whether the current job is Mod Manager install/download, creator upload/publish, catalog/search, or Agent Gateway. The API compatibility namespace is https://moddingflow.com/api/v1. For creator automation, npm run api:cli -- auth check validates the environment credential, and npm run api:cli -- publish --mod-id <id> --archive <path> --version <version> runs validation, direct upload, scan polling, and publish. Separate upload create, resume, complete, and status commands use an explicit protected state file.

/searchapi:cli/publish/downloadhttps://moddingflow.comhttps://moddingflow.com/v1https://moddingflow.com/api/v1

Anonymous curl with no API key, Bearer token, or special headers: curl -X POST https://moddingflow.com/v1/install-plans:resolve -H "Content-Type: application/json" -d "{\"game_slug\":\"skyrim-se\",\"game_version\":\"1.6.1170\",\"mod_ids\":[\"11111111-1111-4111-8111-111111111111\"],\"include_optional\":false}". TypeScript: const client = new ModdingflowPublicApiClient(); await client.resolveInstallPlan({ body: { game_slug: "skyrim-se", game_version: "1.6.1170", mod_ids: [modId] } }); C#: var plan = await client.ResolveInstallPlanAsync(new ResolveInstallPlanRequest { GameSlug = "skyrim-se", GameVersion = "1.6.1170" }); Python: client = ModdingflowPublicApiClient(); plan = client.resolve_install_plan(body={"game_slug":"skyrim-se","game_version":"1.6.1170"}).

/jsonhttps://moddingflow.com/v1/install-plans:resolve

Creator automation starts with POST https://moddingflow.com/v1/uploads, sends bytes directly to storage, refreshes multipart signed parts when needed, polls GET https://moddingflow.com/v1/uploads/{upload_id}, completes the upload, waits for ready or failed, then creates a version draft and publishes. Keep a single Idempotency-Key per logical create/update operation so retries do not duplicate drafts.

/updateIdempotency-Keyhttps://moddingflow.com/v1/uploadshttps://moddingflow.com/v1/uploads/{upload_id}

Reference, SDK, and package policy

3

Use the API reference page for the endpoint list, request/response schemas, scopes, servers, deprecation metadata, and examples generated from OpenAPI.

/response

Until packages are published, the SDK is a repo-only source artifact. Open the SDK source page for source archive links and connection snippets, then use repo/local imports: TypeScript imports sdk/typescript/src/moddingflow-public-api.ts, C# references sdk/csharp/Moddingflow.PublicApi, and Python adds sdk/python to PYTHONPATH before importing sdk/python/moddingflow_public_api. SDK source is not published to npm, PyPI, or NuGet yet. Localized text fields such as title, summary, and description stay language-code maps: TypeScript exposes [key: string]: string, Python exposes dict[str, str], and C# exposes Dictionary<string, string>. Python request DTOs preserve OpenAPI required fields with Required[...] and NotRequired[...], so type checkers flag install-plan bodies missing game_slug or game_version. Generated clients make at most three attempts by default and retry only safe/idempotent methods or writes with an Idempotency-Key. They honor Retry-After and RateLimit-Reset with bounded jittered exponential backoff, expose final request/trace IDs, and can refresh expired signed download URLs through an explicit resolve hook. Regenerate and verify local artifacts with npm run api:sdks:generate and npm run api:sdks:check. The release gate also runs npm run api:sdks:compile, which locally packs and installs npm, wheel, and NuGet artifacts into real consumers and validates the CLI and GitHub Action without publishing registry packages.

The support/EOL matrix in docs/public-api/SDK-SUPPORT-AND-EOL.md records each SDK, CLI, and Action version separately from contract release 1.0.0-foundation, plus runtime floors and distribution state. Current artifacts are Preview source distributions with no registry availability, SLA, signature, or stable EOL promise. A stable support window starts only after a clean immutable publication updates that matrix with dated commitments.

/EOL/local/tracesdk/python/idempotentapi:sdks:checkIdempotency-Keyapi:sdks:compileapi:sdks:generatesdk/csharp/Moddingflow.PublicApisdk/python/moddingflow_public_api/public-api/SDK-SUPPORT-AND-EOL.mdsdk/typescript/src/moddingflow-public-api.ts

Backwards compatibility policy: stable v1 old clients keep working through the support window. Public operationIds, fields, scopes, error codes, headers, cursor semantics, idempotency, and lifecycle behavior cannot be removed, renamed, or changed to an incompatible type without a new major version or a completed deprecation path. Compatible additive fields can ship in the current v1 contract when old clients can ignore them; new request fields must be optional, nullable, or have defaults. New endpoints can be added under v1 when they do not replace an existing stable flow or become a hidden prerequisite. New enum/status values require tolerant client guidance, docs, and SDK updates. Beta or experimental endpoints must be marked with x-moddingflow-stage=beta, x-moddingflow-stage=experimental, or live under a future /v1/beta/* path, and they must not be required for stable flows. Deprecated operations and HTTP-visible capabilities use the Deprecation response header as an RFC 9745 Structured Field date, Sunset as an RFC 8594 HTTP-date, and Link rel="deprecation" for the migration guide. Breaking removals require at least 90 days of warning or two stable API releases, a migration guide with replacement, detection, rollout, deadline, and support contact, plus a dedicated changelog entry. Any deprecated OpenAPI operation must include x-deprecation-date, x-sunset-date, x-migration-url, and x-changelog-url; the OpenAPI lint gate rejects incomplete deprecation metadata. The repository release gate also rejects Sunset unless the exact dates are bound to committed migration, ready replacement, dated telemetry, and approved support-owner evidence.

Release notes, migration guide templates, and stable v1 changelog evidence are tracked on the API changelog.

/status/v1/beta/stage=betastage=experimental

Webhook automation

1

Use POST /v1/webhook-subscriptions with webhooks:write to create an owner-scoped subscription for mod.updated, version.published, file.ready, file.failed, upload.completed, scan.completed, token.changed, app.changed, or account.access.changed. Targets must use public HTTPS on port 443. The API returns the Vault-backed whsec_ signing_secret once; POST /v1/webhook-subscriptions/{subscription_id}/rotate-secret returns a new version with a bounded overlap window. Verify X-Moddingflow-Signature as HMAC-SHA256 over `${timestamp}.${raw_body}` using X-Moddingflow-Timestamp and reject stale timestamps. The versioned body includes spec_version, stable event_id, delivery_id, type, version, occurred_at, opaque subject IDs, and data. Delivery is at-least-once and unordered: acknowledge quickly, process asynchronously, deduplicate by event_id, and fetch the latest state. account.access.changed contains only its event id, monotonic access revision, time, and change categories; fetch /me after it instead of trusting an ordered entitlement snapshot. Automatic retries use exponential backoff and end in exhausted. Manual redelivery through POST /v1/webhook-deliveries/{delivery_id}/redeliver with Idempotency-Key creates a new delivery_id linked to the original while preserving event_id. GET /v1/webhook-deliveries with webhooks:read exposes delivery_status, attempt_count, next_attempt_at, last_response_status, bounded latency, sanitized response snippet, retry state, and links. Repeated terminal failures can disable the subscription.

For upload trust transitions, scan.completed records the immutable scan outcome, including quarantine. A later manual approval emits a separate file.ready event, so neither logical event is overwritten and receivers can deduplicate each event independently.

GET /v1/webhook-deliveriesPOST /v1/webhook-subscriptionsPOST /v1/webhook-deliveries/{delivery_id}/redeliverPOST /v1/webhook-subscriptions/{subscription_id}/rotate-secret/mewebhooks:readwebhooks:writeIdempotency-Key

Operations, limits, and support

4

A successful version mutation response is the authoritative committed result of that operation and is always private, no-store. Anonymous catalog/version GET revalidates shared cache, keeps a weak representation ETag, and uses public, max-age=0, s-maxage=0, no-cache, must-revalidate. Catalog freshness SLO is p95 at most 15 seconds and search-index freshness is p95 at most 1800 seconds; the dashboard uses each objective's own window and includes the age of unfinished jobs, so it can honestly remain critical after recovery. Never treat catalog visibility as confirmation of the commit or delete historical evidence to make an SLO green. Mod/ModVersion PATCH requires one strong quoted If-Match from create, authenticated detail, or the previous PATCH: missing returns 428 precondition_required; malformed, weak, wildcard, or list values return 412 precondition_failed without mutation; a syntactically valid stale validator returns 412 with the current ETag. After 412, fetch detail, rebase the change, and use the new ETag with a new Idempotency-Key; never blind-retry.

The cached PublicModVersion artifact expansion requires actual AV scan_status=clean, lifecycle=ready, and publication state=published and remains eventual. Direct download resolve and install-plan resolve are private point-in-time reads over an eligible target whose established database state may be approved or published. Direct eligibility is not published-only and not legacy-only; request resolve again if a version, artifact, dependency, entitlement, or publication state may have changed.

GET /v1/uploads/{upload_id} returns current database state at request time, while worker transitions are eventual. Poll upload status and job.statusEndpoint until a terminal state. Provider progress is authoritative only when progress.available=true; progress.available=false means unknown, not zero bytes and not loss of accepted multipart parts.

An install plan is a deterministic private/no-store request-time snapshot with an ETag, not a live subscription. Resolve it again after a later version, artifact, dependency, entitlement, or publication change.

Webhooks are feature-gated. When management and delivery are enabled, delivery is eventual, at-least-once, replayable, and unordered: return 2xx quickly, deduplicate by event_id, tolerate replay and reordering, and fetch the latest state. Webhook arrival does not acknowledge a mutation; while either gate is disabled, no delivery is promised.

GET /v1/uploads/{upload_id}age=0/versionmaxage=0/no-store/ModVersionstatus=cleanavailable=trueIdempotency-Keylifecycle=readystate=publishedavailable=false

The pipeline fails closed and claims no cross-provider transaction. Create first commits a once-only byte-budget checkpoint for one request-hash generation, then atomically reserves session/file intent with the upload.accepted audit before provider work. A lost response between those checkpoints replays the same budget result, so exact replay neither charges nor audits twice. Every single PUT is signed with If-None-Match: *; its maximum 900-second URL lease is row-lock checkpointed after signing and before response, and an abort-winning checkpoint rejects the URL. A conditional 412 is an ambiguous existing write: the client calls complete, where server HEAD, the immutable seal, and SHA-256 are authoritative. After provider completion, one session-level claim conditionally copies the exact client-source ETag into a write-once server-only seal. The seal key is persisted before CopyObject, a lost copy response is recovered by exact HEAD validation, and competing completion keys cannot overwrite the bound seal. The immutable identity enters the queued scan checkpoint with SHA pending; the fenced worker streams and atomically attests the full seal hash before validation, AV, or final copy. After a lost response, retry the same normalized request with the same Idempotency-Key; once the checkpoint and job exist, poll upload status.

A content-addressed final copy is not ready yet. Same-SHA promotion is serialized by a durable SHA/bucket/key claim: the winner owns the canonical MIME and uses Cloudflare header cf-copy-destination-if-none-match: *, while a fresh duplicate waits or adopts the exact ready blob without consuming a normal scan attempt. Contention polling grows exponentially from one to 30 seconds and is bounded to 15 minutes per claim. The first two durable token-fenced yields atomically restore the normal attempt and requeue with database-owned 30-to-60-second jitter; the third atomically creates manual review and quarantine instead of retrying forever. Lost-response replay returns the same persisted queued or quarantined outcome. A lost copy response is accepted only after HEAD validates SHA metadata, size, canonical MIME, and a nonblank provider ETag. An accessible attested blob must exist before an atomic ready binding links the immutable blob, logical file, and upload session together with its audit and file.ready event. During manual review, quarantine remains durable through provider attestation: approval atomically commits the ready binding, review resolution, decision audit, and event, while rejection atomically commits the rejected session, file, review, decision audit, and event. Transactional publish and its durable outbox commit in one database transaction; metadata cannot publish before the ready binding.

Heartbeat-backed claim tokens fence every scan mutation: a stale worker cannot change retry, clean, quarantine, rejection, audit, or event state after reclaim. Each new ready, quarantine, or rejection transition commits session, file, job, audit, and the deterministic canonical event in one database transaction. Exact replay validates the existing terminal envelope; after an ambiguous RPC response the worker reloads and accepts only that exact envelope. Bounded reconciliation repairs only explicit legacy gaps while respecting active claim ownership. Deployed webhook delivery is not promised. Account-inbox notifications are deduplicated attempts, not lifecycle acknowledgements; machine-readable upload status remains authoritative.

Before provider configuration or normal scan claims, a bounded database-only sweep recovers stale max-attempt processing claims without rerunning storage or scan work. Coherent state atomically enters canonical manual review and quarantine; incoherent state terminal-fails only the exact token-fenced job with manual_intervention_required, so it stays visible without fabricating session or file state. One bad row cannot block independent jobs, and recovered rows consume the same bounded worker batch budget.

Recovery first retries the proven seal, content-addressed copy, or binding. Cleanup has its own claim token. For single staging, durable stage 1 conditionally writes a zero-byte tombstone and provider-verifies its ETag, LastModified, metadata token, and size=0; the tombstone is held through the persisted latest URL lease and at least 16 minutes. Durable stage 2 performs the exact delete, HEAD absence check, and DB completion; crash replay resumes the current stage. Multipart cleanup aborts and relists every exact-key upload, and every seal claim key is deleted and HEAD-verified; expired uploaded sessions without a checkpoint are included while quarantine retains its seal. Generation byte-budget reservations are purged only after the replay window plus a safety margin. The desired CORS policy includes If-None-Match, and the object-level probe verifies conditional copy plus late-PUT tombstone rejection, but live CORS/lifecycle/lock evidence remains BLOCKED pending operator inputs CLOUDFLARE_API_TOKEN, MODDINGFLOW_R2_FINAL_LOCK_MIN_SECONDS, and MODDINGFLOW_R2_ALLOWED_BROWSER_ORIGINS; no live deployment is claimed. An unbound final copy remains non-ready and non-publishable, is never publishable, and stays under retention/lock until PR-F3.13 reference-aware deletion. Numeric freshness remains PR-F2.6.3, and deployed webhook enablement remains PR-F8.14.

/file/locksize=0/bucket/keyIdempotency-Key/lifecycle/lock

The API applies route, scope, body-size, byte-budget, download concurrency, and abuse limits before expensive side effects. Download leases allow 2 anonymous or 4 authenticated requests per actor and 16 per OAuth/PAT client, expiring after 300 seconds; byte buckets are 24 GiB anonymous, 40 GiB authenticated, and 24 GiB upload. Multipart defaults to 16 MiB × 4, with at most four signed parts per request and 64 MiB payload in flight. Oversized bodies return 413. Abusive or overloaded clients receive 429 with Retry-After and RateLimit headers plus retryable, retry_after_seconds, and a non-sensitive quota_bucket. Every application/problem+json error also includes code, docs_slug, request_id, trace_id, and X-Request-Id; scope and validation errors add required_scopes or fields only when actionable. Error codes are additive within v1, so clients must preserve unknown codes and fall back to numeric HTTP status.

/PAT/problem

Before relying on a new API artifact, check that OpenAPI, SDK generation, compatibility checks, localized docs, and reference rendering all passed for the release. Stable v1 also requires contract_test_matrix coverage: DTO/schema, Problem Details, cursor pagination, upload-session lifecycle, download-grant lifecycle, OAuth/PAT scopes, webhook signature, webhook retry/redelivery, backwards compatibility, and client flow smoke. It also requires final_readiness_checklist coverage: public endpoints documented, opaque ID entity addressing, signed URL/resumable upload, download-grant downloads, multiple hash algorithms, async status/job endpoints, OAuth/PAT self-service, Problem Details, cursor-based pagination, webhook signatures and redelivery, Deprecation/Sunset policy, current OpenAPI spec, current FAQ and public documentation, passed tests and validations, known limitations list, and first stable release notes. If an endpoint is absent from OpenAPI, treat it as website-internal rather than public developer surface.

/PAT/job/schema/Sunset/resumable/redelivery
Moddingflow

Community, mods, and discussions for favorite games.

Project

NewsRatingAPIOAuthRules

Products

Moddingflow Premium

Site documents

Terms of UsePrivacy PolicyCookie PolicyLegal NoticeWithdrawal Information

Support

FAQ

© 2026 Moddingflow. All rights reserved.

Moddingflow
BETA

Games

All games (13)Recently added

My games

Oblivion RemasteredMinecraftStarfieldFallout 3The Witcher 3Oblivion
ForumNewsRating