Moddingflow API

Mod Manager install and download

1. OAuth: open https://api.moddingflow.com/oauth/authorize with PKCE, then exchange the code at https://api.moddingflow.com/oauth/token. 2. List or search: call GET https://api.moddingflow.com/v1/search?q=skyui&game=skyrim-se or GET https://api.moddingflow.com/v1/mods. 3. Install plan: call POST https://api.moddingflow.com/v1/install-plans:resolve with game_slug, game_version, and a mod, version, or artifact UUID. 4. Download resolve: call POST https://api.moddingflow.com/v1/downloads/{artifact_id}/resolve for each plan artifact. 5. Verify SHA-256: compare the completed file with hashes.sha256 or the legacy sha256 alias returned by resolve before installing. 6. Retry: on 429, 5xx, expired signed URL, or failed hash, back off, refresh the resolve response, and use fallback only when the response allows it.

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.

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, conditional requests, and resume. Its value may change after refresh or provider failover, so never carry an old ETag/If-Match into fallback. 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.

Access is limited by user/auth context, client/app identity, files:download or delegated download scope, entitlement checks, 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 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.

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.

Creator upload and publish automation

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.

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 and executables require manual 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.

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.

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.

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.

Catalog and search integrations

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 and defaults sort to relevance, while 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.

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.

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, 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. 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.

Agent Gateway delegated tools

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

API base URL: https://api.moddingflow.com/v1. OAuth issuer URL: https://api.moddingflow.com. Website compatibility namespace: https://www.moddingflow.com/api/v1. 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. Use the website compatibility namespace only when a browser-hosted integration must stay on www.moddingflow.com.

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.

The desktop Mod Manager and user-created public applications open https://api.moddingflow.com/oauth/authorize with response_type=code, an S256 code_challenge, requested scopes, state, and an allowlisted redirect URI. The user sees the application and requested scopes and must explicitly approve or deny consent. Sensitive scopes require prior application review and a recent AAL2 2FA step-up. The client exchanges the authorization code at https://api.moddingflow.com/oauth/token and stores only the resulting tokens, never the user password. To receive an identity assertion, request openid with a fresh 8-512 character nonce; the token response then includes a five-minute RS256 ID token whose signature, issuer, client_id audience, iat/exp, and nonce must all be validated. Never use the ID token as the /v1 Bearer access token.

Refresh tokens rotate on every refresh. If a client reuses an older refresh token, the server immediately revokes the entire refresh family and requires a new authorization.

CLI and fallback clients create a device grant at https://api.moddingflow.com/oauth/device_authorization, show the returned user_code and verification URI, then poll https://api.moddingflow.com/oauth/token. The verification page shows the client and requested scopes and lets the user approve or deny. Poll no faster than interval, increase backoff after slow_down, and stop on expired_token or access_denied.

Developers register OAuth applications in Settings > Developer at /account/developer/applications. A new public client receives a client_id, registers exact HTTPS, custom-scheme, or loopback redirect URIs, chooses Device Flow support, uses PKCE S256, and can start with basic read/install scopes without manual moderation: mods:read, files:download, install_plans:resolve, and profile:read. Every user sees explicit consent for the requested scopes and an account-wide resource statement: approved OAuth permissions cover all resources the user is eligible to access. Use a resource-scoped PAT when automation must be restricted to selected mods or projects.

Sensitive grants require review before they can be issued to the app: mods:write, versions:write, files:upload, webhooks:write, private_data:read, agent:* and admin:*. Granting sensitive scopes also requires a recent AAL2 2FA step-up. Unknown scopes fail closed, and admin:* is never available to user-created applications. Disabling an application takes effect immediately for authorization, token issuance, refresh, and access-token checks.

Send User-Agent as ApplicationName/ApplicationVersion and include a maintained contact URL or email in its comment. PAT mutations require both X-Moddingflow-Client and X-Moddingflow-Client-Version. OAuth client_id is authoritative, so a supplied application-name header must match it. Missing, generic, malformed, or mismatched identity returns 400 invalid_client_identity before quota consumption or business side effects. Official TypeScript, Python, C#, CLI, and GitHub Action clients send stable identity headers by default.

Individual API keys and Personal Access Tokens are for user-owned automation. POST /v1/api-keys and POST /v1/api-keys/{api_key_id}/rotate require Idempotency-Key. The same key and body never repeat the mutation; if the one-time secret can no longer be recovered, the replay fails closed with idempotency_replay_unavailable and no operation-shaped committed metadata. Inspect the key through GET /v1/api-keys or revoke it before using a new idempotency key. POST /v1/api-keys reveals the raw secret exactly once; storage keeps only token_hash, a safe fingerprint/display_prefix, scopes, resource grants, last_used_at, expires_at, and rotation/revoke state. GET /v1/api-keys returns safe metadata only, while GET /v1/api-keys/capabilities shows available scopes, mod projects, and the team role before key creation. The same operations are available to signed-in authors in Settings > Developer without exposing the account session cookie to external tools.

POST /v1/api-keys/{api_key_id}/rotate atomically creates a replacement with the same scopes, resource grants, and expiry and reveals the new secret once. The previous secret remains valid only for a maximum 10-minute overlap, then is revoked automatically. DELETE /v1/api-keys/{api_key_id} performs an immediate idempotent revoke. The owner manages team publishing through /v1/mods/{mod_id}/team-members: owner, maintainer, publisher, and reviewer use their own user identities, while the audit log stores key id/fingerprint but never a secret.

Request only the scopes needed for the persona: mods:read for catalog, mods:write for metadata, versions:write for version drafts, files:upload for upload, versions:publish for publish, versions:archive for archive/rollback, files:manage for file management, and teams:admin for owner-only team management. files:download and install_plans:resolve cover Mod Manager installs, webhooks:read/webhooks:write cover webhook management, private_data:read covers private user-owned data, and agent:* covers delegated tools. admin:* is reserved for trusted internal clients.

OAuth, PAT, upload, download, and Agent Gateway calls are rate-limited before storage, signing, upload, download, or tool side effects. Buckets are evaluated per application/client_id, per user/actor_user_id, and per token_id_or_api_key_id, with scope and token_kind included in policy matching.

On 429, clients must respect Retry-After and any RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset headers. A different PAT or OAuth app must not be used to bypass a user or token abuse bucket.

Runnable examples

Open the OpenAPI JSON, configure apiBaseUrl https://api.moddingflow.com/v1 and oauthIssuerUrl https://api.moddingflow.com, then choose whether the current job is Mod Manager install/download, creator upload/publish, catalog/search, or Agent Gateway. The website compatibility namespace is https://www.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.

curl -X POST https://api.moddingflow.com/v1/install-plans:resolve -H "Authorization: Bearer $ACCESS_TOKEN" -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({ accessToken }); 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(access_token="TOKEN"); plan = client.resolve_install_plan(body={"game_slug":"skyrim-se","game_version":"1.6.1170"}).

Creator automation starts with POST https://api.moddingflow.com/v1/uploads, sends bytes directly to storage, refreshes multipart signed parts when needed, polls GET https://api.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.

Reference, SDK, and package policy

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

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.

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.

Webhook automation

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, or app.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. 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.

Operations, limits, and support

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.

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.

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.

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.