Publish new module versions to FoundryMods from your CI pipeline. Drop-in compatible with Foundry's Package Release API — same request shape, same response envelope.
Not a developer? You don't need a terminal.
Open your module's edit page and use the "Test the Release API" panel to fire a safe, no-publish test request with one click — perfect for confirming a token works before handing it to GitHub Actions.
fmp_… token.FOUNDRYMODS_TOKEN).Tokens are shown once. Lose it? Just revoke and generate a new one.
https://foundrymods.com/api/public/v1/packages/release_versionAlways test with "dry-run": true first — it validates everything without saving.
release.manifest, thenid,release.version, andrelease.compatibility.* are derived from the manifest JSON and become optional in the request body. If you send them anyway, they must match the manifest exactly. If you omit the manifest, all four are required.# Minimal — id, version, and compatibility are derived from the manifest
curl -X POST https://foundrymods.com/api/public/v1/packages/release_version \
-H "Content-Type: application/json" \
-H "Authorization: fmp_your_token_here" \
-d '{
"dry-run": true,
"release": {
"manifest": "https://github.com/me/my-module/releases/download/1.2.0/module.json",
"notes": "https://github.com/me/my-module/releases/tag/1.2.0"
}
}'# Manifest omitted — id, version, and compatibility become required
curl -X POST https://foundrymods.com/api/public/v1/packages/release_version \
-H "Content-Type: application/json" \
-H "Authorization: fmp_your_token_here" \
-d '{
"id": "my-module",
"dry-run": true,
"release": {
"version": "1.2.0",
"notes": "https://github.com/me/my-module/releases/tag/1.2.0",
"compatibility": { "minimum": "11", "verified": "12" }
}
}'release.manifestURL to the version-specific manifest JSON (e.g. a GitHub release asset). Strongly recommended — when present, the fields below are derived from it and become optional.
idThe module id from your manifest. Optional if a manifest URL is provided (derived from manifest.id). Required otherwise. Must match the module that owns this token.
dry-runWhen true, validates without saving.
release.versionSemantic version, e.g. 1.2.0. Optional if a manifest URL is provided (derived from manifest.version). Required otherwise. Must be unique per module.
release.notesOptional URL to release notes / changelog page. Falls back to manifest.url when omitted.
release.compatibility.minimumEarliest Foundry version supported. Optional if a manifest URL is provided (derived from manifest.compatibility.minimum). Required otherwise.
release.compatibility.verifiedLatest Foundry version verified to work. Optional if a manifest URL is provided (derived from manifest.compatibility.verified). Required otherwise.
release.compatibility.maximumLatest Foundry version this release will run on (omit unless you've tested a hard stop).
FoundryMods extensions (optional)
release.changelogInline markdown changelog displayed on the module page.
release.is_publicSet to false for an unlisted prerelease.
release.packageURL to the release zip. When provided, FoundryMods mirrors the file to its own CDN, hosts the download, and indexes contents (compendium packs + scene gallery).
release.foundrymods.required_tier_idsPatreon tier IDs required to download. Gated releases never expose a public manifest URL.
release.foundrymods.required_bw_tier_idsBaileywiki Central tier IDs required to download.
release.foundrymods.lapsed_subscriber_accessDefines what former subscribers retain. Default: none.
release.foundrymods.monthly_download_limitPer-user download cap (1–20). Default: 4.
release.foundrymods.is_unlistedHide from the gallery while still allowing direct-link installs.
release.foundrymods.discord_announcePost a release announcement to the FoundryMods Discord. Public releases only.
release.foundrymods.sync_description_from_githubAfter a successful release, re-pull your repository's README.md and replace this module's description with it. You can also enable this once per module from the edit page (Auto-sync on every release), or trigger it per-request via the X-FM-Sync-Description: 1 header (also accepted by the direct ZIP upload endpoint). Set this field to false to skip a sync even if auto-sync is enabled module-wide.
Lookup endpoint (GET)
GET https://foundrymods.com/api/public/v1/packages/release_version?id=<manifest-id> lists the latest 50 releases. Add &version=1.2.0 to fetch a single release. No token required for read access.
{ "status": "success", "page": "https://foundrymods.com/modules/.../edit" }{
"status": "error",
"errors": {
"manifest": [{ "message": "Enter a valid URL.", "code": "invalid" }]
}
}Missing, malformed, or revoked token.
The module owner does not have an active FoundryMods Creator subscription, or the upload would exceed their storage quota. Response includes code: "subscription_required" or code: "quota_exceeded" plus a subscribe_url.
Token scope insufficient, or the module has no active owner claim (code: "no_owner_claim").
One release per module per 60 seconds. Check the Retry-After header.
name: Release
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Publish to FoundryMods
run: |
curl -X POST https://foundrymods.com/api/public/v1/packages/release_version \
-H "Content-Type: application/json" \
-H "Authorization: ${{ secrets.FOUNDRYMODS_TOKEN }}" \
-d @- <<EOF
{
"id": "my-module",
"release": {
"version": "${{ github.event.release.tag_name }}",
"manifest": "https://github.com/${{ github.repository }}/releases/download/${{ github.event.release.tag_name }}/module.json",
"notes": "${{ github.event.release.html_url }}",
"compatibility": { "minimum": "11", "verified": "12" },
"changelog": ${{ toJSON(github.event.release.body) }}
}
}
EOFIf your CI builds the zip on the runner and you don't want to push it to a separate host first, stream it straight to FoundryMods. We'll store it on our CDN, create the release, and run the same content indexing.
https://foundrymods.com/api/public/v1/packages/upload_zipBody is the raw zip bytes (up to 2 GB). All metadata travels in X-FM-* headers so no multipart parsing is needed. Add X-FM-Dry-Run: true to validate first.
foundrymods addon (otherwise subscription_required).storage_quota_gb entitlement (otherwise quota_exceeded).file_size across every release uploaded by that owner email — including releases created via the JSON release endpoint and the browser uploader.no_owner_claim.subscribe_url the owner can visit to upgrade.X-FM-Manifest-Url is always required. When set, X-FM-Id,X-FM-Version,X-FM-Compat-Min, andX-FM-Compat-Verified are derived from the manifest JSON and become optional. If you send any of them, they must match the manifest exactly.# Minimal — id, version, and compatibility are read from the manifest curl -X POST https://foundrymods.com/api/public/v1/packages/upload_zip \ -H "Authorization: fmp_your_token_here" \ -H "Content-Type: application/zip" \ -H "X-FM-Manifest-Url: https://github.com/me/my-module/releases/download/1.2.0/module.json" \ -H "X-FM-Notes-Url: https://github.com/me/my-module/releases/tag/1.2.0" \ --data-binary "@dist/my-module-1.2.0.zip"
X-FM-Manifest-Url — requiredX-FM-Id, X-FM-Version, X-FM-Compat-Min, X-FM-Compat-Verified — optional when set in the manifest, otherwise requiredX-FM-Compat-Max — optionalX-FM-Notes-Url, X-FM-Filename, X-FM-Changelog-B64 (base64 markdown)X-FM-Required-Tier-Ids, X-FM-Required-Bw-Tier-Ids (comma-separated)X-FM-Lapsed-Access (none|locked_version|full_updates)X-FM-Monthly-Limit (1–20), X-FM-Unlisted, X-FM-Discord-AnnounceX-FM-Dry-RunIf you already publish to Foundry's release API, you only need to change two things:
https://foundrymods.com/api/public/v1/packages/release_versionfmp_… token (instead of fvttp_…)The request body, response envelope, and error shape are identical. Existing tooling like FVTT Autopublish and foundry-publish work with a single env var swap.