Knowledge base

Projects / Uploading through a script

If you want to automate your uploading process through CurseForge, there is a way to do this properly:

Getting your API Key

First, you must go to and generate an API key if you have not already. This is unique to you and provides you a way to authenticate through scripts without a complex login process.

Figuring out your game versions

Next, for the game you want, you're going to want to calculate what game versions you support, knowing the backend ID. Thankfully, we provide an API call for you:

And so forth, changing the domain based on the game you want. This will return a JSON object of the following form:

{"1": // this is the id of the game version
    {"is_development": false, // true if it's in PTR but not officially released
     "breaks_compatibility": false, // change between, e.g. 2.4 and 3.0, but not 3.0 to 3.1
     "release_date": "2006-09-26", // "YYYY-MM-DD" or null
     "name": "1.12.0", // name of the version
     "internal_id": "11200"}} // if available, an internal version. For WoW, this is the TOC number. null if not available

You need to calculate 1-3 IDs of game versions that the file is compatible with.

Actually uploading

Now to upload your file.

Find your upload file page, e.g. Change the final slash to .json, and you have

Now you make a POST request to that URL with the following url-encoded params:

The name of the file you're uploading, this should be the version's name, do not include your project's name.
Specify 1-3 times with the IDs you attained previously.
Specify a for Alpha, b for Beta, and r for Release.
The change log of the file. Up to 50k characters is acceptable.
Markup type for your change log. creole or plain is recommended.
Optional. The known caveats of the file. Up to 50k characters is acceptable.
Markup type for your known caveats. creole or plain is recommended.
The actual zip file for your addon.

There are a few results of your request:

403 Forbidden
You didn't specify your API Key correctly or you do not have permission to upload a file to that project.
404 Not Found
Project couldn't be found. You either specified it wrong or it was renamed.
405 Method Not Allowed
You did a GET instead of a POST.
422 Unprocessable Entity
You have an error in your form. This is a JSON response that will tell you which fields had an issue.
201 Created
Hurrah, your file is uploaded properly.

Example code in python

#!/usr/bin/env python

from httplib import HTTPConnection
from os.path import basename, exists
from mimetools import choose_boundary
    import simplejson as json
except ImportError:
    import json

def get_game_versions(game):
    Return the JSON response as given from /game-versions.json from of the given game
        The shortened version of the game, e.g. "wow", "war", or "rom"
    conn = HTTPConnection('%(game)' % { 'game': game })
    conn.request("GET", '/game-versions.json')
    response = conn.getresponse()
    assert response.status == 200, "%(status)d %(reason)s from /game-versions.json" % { 'status': response.status, 'reason': response.reason }
    assert response.content_type == 'application/json'
    data = json.loads(
    return data

def upload_file(api_key, game, project_slug, name, game_version_ids, file_type, change_log, change_markup_type, known_caveats, caveats_markup_type, filepath):
    Upload a file to on your project
        The api-key from
        The shortened version of the game, e.g. "wow", "war", or "rom"
        The slug of your project, e.g. "my-project"
        The name of the file you're uploading, this should be the version's name, do not include your project's name.
        A set of game version ids.
        Specify 'a' for Alpha, 'b' for Beta, and 'r' for Release.
        The change log of the file. Up to 50k characters is acceptable.
        Markup type for your change log. creole or plain is recommended.
        The known caveats of the file. Up to 50k characters is acceptable.
        Markup type for your known caveats. creole or plain is recommended.
        The path to the file to upload.
    assert len(api_key) == 40
    assert 1 <= len(game_version_ids) <= 3
    assert file_type in ('r', 'b', 'a')
    assert exists(filepath)
    params = []
    params.append(('name', name))
    for game_version_id in game_version_ids:
        params.append(('game_version', game_version_id))
    params.append(('file_type', file_type))
    params.append(('change_log', change_log))
    params.append(('change_markup_type', change_markup_type))
    params.append(('known_caveats', known_caveats))
    params.append(('caveats_markup_type', caveats_markup_type))

    content_type, body = encode_multipart_formdata(params, [('file', filepath)])
    headers = {
        "User-Agent": "CurseForge Uploader Script/1.0",
        "Content-type": content_type,
        "X-API-Key": api_key}
    conn = HTTPConnection('%(game)' % { 'game': game })
    conn.request("POST", '/projects/%(slug)s/upload-file.json' % {'slug': project_slug}, body, headers)
    response = conn.getresponse()
    if response.status == 201:
        print "Successfully uploaded %(name)s" % { 'name': name }
    elif response.status == 422:
        assert response.content_type == 'application/json'
        errors = json.loads(
        print "Form error with uploading %(name)s:" % { 'name': name }
        for k, items in errors.iteritems():
            for item in items:
                print "    %(k)s: %(item)s" % { 'k': k, 'name': name }
        print "Error with uploading %(name)s: %(status)d %(reason)s" % { 'name': name, 'status': response.status, 'reason': response.reason }

def encode_multipart_formdata(fields, files):
    Encode data in multipart/form-data format.
        A sequence of (name, value) elements for regular form fields.
        A sequence of (name, filename) elements for data to be uploaded as files
    Return (content_type, body) ready for httplib.HTTP instance
    boundary = choose_boundary()
    L = []
    for key, value in fields:
        if value is None:
            value = ''
        elif value is False:
        L.append('--%(boundary)s' % {'boundary': boundary})
        L.append('Content-Disposition: form-data; name="%(name)s"' % {'name': key})
    for key, filename in files:
        f = file(filename, 'rb')
        filedata =
        L.append('--%(boundary)s' % {'boundary': boundary})
        L.append('Content-Disposition: form-data; name="%(name)s"; filename="%(filename)s"' % { 'name': key, 'filename': basename(filename) })
        L.append('Content-Type: application/zip')
    L.append('--%(boundary)s--' % {'boundary': boundary})
    body = '\r\n'.join(L)
    content_type = 'multipart/form-data; boundary=%(boundary)s' % { 'boundary': boundary }
    return content_type, body

You must login to post a comment. Don't have an account? Register to get one!

  • Avatar of lovesamrat lovesamrat Feb 20, 2016 at 12:15 UTC - 0 likes

    Really a great and impressive information.

  • Avatar of codegreen codegreen Dec 08, 2015 at 18:33 UTC - 0 likes

    Is there a way to upload files this way for minecraft, this post is out dated.

  • Avatar of budakleuweung budakleuweung Sep 11, 2015 at 01:01 UTC - 0 likes

    thanks dude for sharing this tutorial with us :)

  • Avatar of sedotwc sedotwc Aug 24, 2015 at 13:51 UTC - 0 likes

    Thanks for share...

  • Avatar of robotbrain robotbrain Jan 28, 2014 at 21:20 UTC - 0 likes

    Now one question: how do I get a download link for the uploaded file automatically? Oh whoops it has a location header in the response

    Last edited Jan 28, 2014 by robotbrain
  • Avatar of storm345 storm345 Jan 27, 2014 at 17:52 UTC - 0 likes

    Hey, I use the API in my Bukkit Plugin, UltimatePluginUpdater, to update all server plugins automatically. However, recently I have been noticing that your service is more frequently returning error 504 and is generally taking longer to respond to requests. I presume this is a result of the service being more used; but is there anything that can be done to make it go faster? (This never used to be an issue!) Thanks :)


  • Avatar of Udorn Udorn Sep 14, 2013 at 10:02 UTC - 0 likes

    I was using the python script a while, but from one day to the next my python installation was broken and the script didn't function anymore. So I wrote a shell script (may be used on windows with cygwin), which may be helpful for other people as well:

    API_KEY="Fill in your API key here"
    curl -F "name=$NAME" -F "game_versions=$GAME_VERSIONS" -F "file_type=$FILE_TYPE" \
        -F "change_log=<$CHANGES_FILE" -F "change_markup_type=$CHANGE_MARKUP_TYPE" \
        -F "known_caveats=$KNOWN_CAVEATS" -F "caveats_markup_type=$CAVEATS_MARKUP_TYPE" \
        -F "file=@$ZIP_FILE" -H "X-API-Key: $API_KEY" "$KEY/upload-file.json"

    For my Addon AuctionMaster the call would be:  r 5.6.0 AuctionMaster ./ ./Changes.txt

    This may be combinded with another script that does all the subversion, toc editing and zip-build stuff.

  • Avatar of feildmaster feildmaster Jan 19, 2013 at 07:44 UTC - 0 likes

    The response wouldn't happen to include the URL or ID to the uploaded file page would it?

    Would it be possible to include such a feature? (To automatically upload, and then provide a link if you wish to view it)

  • Avatar of Jaliborc Jaliborc Jan 06, 2012 at 16:37 UTC - 0 likes

    Using Requests I was able to make a much simpler script in python:

    Last edited Jan 06, 2012 by Jaliborc

    Developer of addons such as Bagnon, PetTracker and OmniCC
    Visit me at



Date created
Apr 16, 2009
Last updated
Jul 12, 2011