Back

Package Release API

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.

Foundry-compatible
Per-module tokens
FoundryMods extras

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.

1. Get a token
  1. Open your module's edit page on FoundryMods (you must own/claim the module).
  2. Find the Package Release Tokens card.
  3. Click Generate and copy the fmp_… token.
  4. Store it in your CI as a secret (e.g. FOUNDRYMODS_TOKEN).

Tokens are shown once. Lose it? Just revoke and generate a new one.

2. Send a request
POST
https://foundrymods.com/api/public/v1/packages/release_version

Always test with "dry-run": true first — it validates everything without saving.

Manifest is the source of truth. If you providerelease.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"
    }
  }'
Without a manifest URL (legacy — all fields required)
# 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" }
    }
  }'
Request body
release.manifest
url

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

id
string

The 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-run
boolean

When true, validates without saving.

release.version
string

Semantic 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.notes
url

Optional URL to release notes / changelog page. Falls back to manifest.url when omitted.

release.compatibility.minimum
string

Earliest Foundry version supported. Optional if a manifest URL is provided (derived from manifest.compatibility.minimum). Required otherwise.

release.compatibility.verified
string

Latest Foundry version verified to work. Optional if a manifest URL is provided (derived from manifest.compatibility.verified). Required otherwise.

release.compatibility.maximum
string

Latest Foundry version this release will run on (omit unless you've tested a hard stop).

FoundryMods extensions (optional)

release.changelog
markdown
FoundryMods

Inline markdown changelog displayed on the module page.

release.is_public
boolean
FoundryMods

Set to false for an unlisted prerelease.

release.package
url
FoundryMods

URL 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_ids
string[]
FoundryMods

Patreon tier IDs required to download. Gated releases never expose a public manifest URL.

release.foundrymods.required_bw_tier_ids
string[]
FoundryMods

Baileywiki Central tier IDs required to download.

release.foundrymods.lapsed_subscriber_access
"none"|"locked_version"|"full_updates"
FoundryMods

Defines what former subscribers retain. Default: none.

release.foundrymods.monthly_download_limit
number
FoundryMods

Per-user download cap (1–20). Default: 4.

release.foundrymods.is_unlisted
boolean
FoundryMods

Hide from the gallery while still allowing direct-link installs.

release.foundrymods.discord_announce
boolean
FoundryMods

Post a release announcement to the FoundryMods Discord. Public releases only.

release.foundrymods.sync_description_from_github
boolean
FoundryMods

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

Responses
200 OK
{ "status": "success", "page": "https://foundrymods.com/modules/.../edit" }
400 Bad Request
{
  "status": "error",
  "errors": {
    "manifest": [{ "message": "Enter a valid URL.", "code": "invalid" }]
  }
}
401 Unauthorized

Missing, malformed, or revoked token.

402 Payment Required

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.

403 Forbidden

Token scope insufficient, or the module has no active owner claim (code: "no_owner_claim").

429 Too Many Requests

One release per module per 60 seconds. Check the Retry-After header.

3. GitHub Actions example
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) }}
            }
          }
          EOF
Direct ZIP upload (CI without GitHub Releases)

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

POST
https://foundrymods.com/api/public/v1/packages/upload_zip

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

Subscription & storage quota required. ZIP uploads are gated by the module owner's FoundryMods Creator subscription. Before accepting the upload, the API resolves the active claim's owner email and checks:
  • The owner has an active foundrymods addon (otherwise
    402
    subscription_required).
  • The new file size plus the owner's existing usage fits within their storage_quota_gb entitlement (otherwise
    402
    quota_exceeded).
  • Usage is the sum of file_size across every release uploaded by that owner email — including releases created via the JSON release endpoint and the browser uploader.
  • The module must have an active claim or the request is rejected with
    403
    no_owner_claim.
Both 402 responses include a subscribe_url the owner can visit to upgrade.
Manifest is the source of truth.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"
All supported headers
  • X-FM-Manifest-Urlrequired
  • X-FM-Id, X-FM-Version, X-FM-Compat-Min, X-FM-Compat-Verified — optional when set in the manifest, otherwise required
  • X-FM-Compat-Max — optional
  • X-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-Announce
  • X-FM-Dry-Run
Migrating from Foundry's API

If you already publish to Foundry's release API, you only need to change two things:

  • Endpoint URL → https://foundrymods.com/api/public/v1/packages/release_version
  • Authorization header → your fmp_… 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.