How to control your games using custom API requests and reset your worlds periodically

Note: This is an advanced use case and mostly meant for systems and module developers who have a demo setup on The Forge, rather than for the average users.

If you have a game hosted that you want to control using The Forge’s API, you can do so by first creating an API Key with the proper permissions.

I will explain here some of the undocumented APIs that you can use on The Forge.

API Keys

You can refer to the FAQ item The Forge API Key Manager on how API Keys work, but you’ll notice that the interface only makes available the ‘read assets’ and ‘write assets’ permissions. There are other undocumented permissions which you can request that give you more control over your game and data.

We’ll talk about two sets of permissions. The first one is manage-games which lets you start/stop/idle a game, the second one is write-data that lets you overwrite your current data/world (note that for security reasons, any data writing API also requires the read-data permission as well).

Getting a new API Key

Since the API Manager doesn’t give access to the broader set of permissions (for good reason, those shouldn’t be used by most users and they have a certain level of risk if someone was to enable them without knowing what they do), we’re going to generate a new API Key manually.
Open your “My Account” page and open the dev console and type :

response = await Utils.api("generate-key", {permissions: ["read-data", "write-data", "manage-games"]})`

You can set the list of permissions you want, in this case we’re asking for the read-data, write-data and manage-games permissions. The key has no expiration date, but you can also set an expiration by adding a expiration field to the API request, with the expiration being expressed in seconds or a string describing a time span zeit/ms (like “1 month”).

The response should have a key field with your API key containing the permissions you requested.
Don’t forget that if the key is lost/leaked, you can always reset all your API keys and invalidate them by using the API Manager UI.

API Details

Now that you have your API key, let’s get down to the gritty details of the Forge API.

Controlling your games

You can now use your API key to start/stop/idle your games. We will not be using the Utils.api helper function anymore (which doesn’t use a custom API Key but instead creates the proper POST request with XSRF tokens to prevent XSS attacks), instead, we’ll simply use the fetch API (feel free to adapt it to use curl instead if that’s what you want) :

response = await fetch("https://forge-vtt.com/api/game/start", {method: "POST", headers: {"Access-Key": key}})
result = await response.json()
if (result.success) console.log("Your table was started successfully")

The start/stop/idle APIs require the manage-games permission and can be used to Start a game, Stop it, or make it go Idle (which stops it if nobody is currently logged in but will automatically start it again if it is accessed).
The response will have either a success field which returns true or false or in case of an error, the response will have an error field instead as a string. If an error is returned, the json will also have a code field which contains the numerical HTTP error code for ease of access/scripting.

Specifying options

If you are on Story Teller tier and you have more than one game (not currently possible, but eventually…) you can specify which game you want to act upon by using the game argument to the API. You can pass arguments along this way :

response = await fetch("https://forge-vtt.com/api/game/idle", {
    method: "POST", 
    headers: {
        "Access-Key": key,
        "content-type": "application/json"
    },
    body: JSON.stringify({game: "game-slug"})
});

Here are the available API descriptions with their options :

/api/game/start

The Start API will start your Foundry server. This means that the Foundry instance will actually start running. This is practical if you had stopped the server and want to start it again, but do note that if the server was previously stopped (not idle), the game will automatically start in Foundry setup page and will be inaccessible to all users.

  • game : Specify the URL slug of the game you want to start. You can of course only specify games that you own. If this value is unset, then your main table will be used.

/api/game/stop

The Stop API stops the server immediately. Anyone currently in a game will lose connection to it and any attempt to reconnect will fail as the server will not auto-launch on access as it would for an idle game. This is a good way to force everybody out and lock it so nobody can access the game anymore. Note that if you are logged in onto The Forge and you have a page open to Foundry, as the game owner, The Forge will automatically re-start the game when you try to access it; only a non-owner will get the error about the game being offline.

  • game : Specify the URL slug of the game you want to sop. You can of course only specify games that you own. If this value is unset, then your main table will be used.

/api/game/idle

The Idle API is one of the most interesting. When you idle a game, it means that the server will be stopped if it was online, but it will be automatically started the next time someone tries to access it. If the game was offline, then it will not be started, but will go into the idle state so that it can be auto-spawned on demand. When idling a game, The Forge will check if anyone is actually logged into it and will only idle the server if no user is logged in. The success response you will receive from the API will let you know if the idling succeeded or not. If you receive a success: false response, it most certainly means that the server is currently in use by a player. You can also force the server to idle despite users being logged in, though when Foundry tries to reconnect the websocket, that will most likely restart the server automatically. You can also use this API to move a game from “offline” to “idle” without actually starting the process, which can be very useful if you stop the server to do some data maintenance then want to make it available again but without needlessly starting the server if nobody is actively going to access it immediately. When idling a game using this API, you can also override the world which The Forge records as being last in use, so this API can also be used to automatically switch Foundry worlds on demand.

  • game : Specify the URL slug of the game you want to idle. You can of course only specify games that you own. If this value is unset, then your main table will be used.
  • force : Force the game to idle, which means that The Forge will not verify that no users are logged in and will stop the Foundry server instance, though it will probably cause any logged in user to reconnect and re-spawn a new process (usefulness doubtful).
  • ‘world’ : If this argument is set, it will override the name (not the title) of the world that The Forge will record as the ‘last world used’, so that the next time the game is launched, it will launch into this world. If no world is specified, then the last world used by the game will be kept (note that it gets reset if the server was set offline). Note that for Story Teller + tiers (once the functionality becomes available), where a game will be locked to a specific world, this argument will be ignored.

A little bonus

For the regular users out there, if you run multiple games and you’re on the Game Master tier and don’t plan to upgrade, or you can’t wait for the Story Teller features to be implemented which would give a unique URL for each of your worlds, yes, you can use the idle API endpoint to easily switch your table from one world to another without having to login/return to setup/launch world manually. If you have the know-how, you could setup a script so it switches to the appropriate world at specific times depending on your weekly D&D schedule.
I’ll leave the details of how to do that as an exercise to the reader :slight_smile:

Overwriting your games

Now that you know how to start, stop and idle your games, you may want to overwrite the data in it automatically. There are two ways to go about this, in both cases, your API Key will need both the read-data and write-data permissions.

Method 1 : Your world is in the Bazaar

Let’s assume that you have a demo world that you just want to reset to its pristine state from the Bazaar, you can use the following API to do so :

/api/data/worlds/install

This will install a world into your Data from the Bazaar directly, it takes two arguments :

  • name: This is the name of the package you want to install, it could be pf2e-demo or kobold-cauldron for example
  • version: The exact version of the package to install. This field is optional, if omitted, the latest version of the package is installed, otherwise, it lets you select a specific version to install

Method 2 : Import your world from a zip backup

If your world isn’t a publicly available world that is available on the Forge’s Bazaar, then you can still import it directly from a ZIP file. Do note however that when importing a world from zip, the world needs to not exist as The Forge will not overwrite it automatically (something that Package installs are not doing).
Let’s assume the use case of importing a new world.
When uploading a zip file, you cannot do it through an application/json content-type request, instead you need to use a multipart/form-data request.

const formData = new FormData();
formData.append("type", 'world')
formData.append("zip", zipBlob)
   
response = await fetch("https://forge-vtt.com/api/data/import", {
    method: "POST", 
    headers: {
        "Access-Key": key,
    },
    body: formData
});

As you notice, it takes 2 arguments, first a type argument that needs to be set to world and a zip argument that needs to be a Blob of the zip file you want to import.

Deleting an existing world

If the world already exists, then you cannot import the ZIP file as The Forge will refuse to overwrite it. You will first need to delete the existing world before you can import a new world with the same name. To delete a world, you can use the /api/data/worlds/delete API which takes 2 arguments :

  • name: The name of the world to delete
  • secret: A secret value that is unique to the world and to the user which authenticates the request for deletion.

The secret value is an important one that is cryptographically generated from a shared secret between all Forge servers, the user ID of the logged in user, and the world’s name. The reason for using it is because, while The Forge is secured against cross-site-scripting with the use of XSRF tokens, and against potential malicious modules that would use the API from within Foundry games themselves (i.e your XSRF tokens will be rejected if the API is called from a Foundry module and you’re not specifically using an API key with the right permissions), I do not want a as-yet-unknown use case I can’t predict in which a website is tricking a user into generating an API call which would call the delete API on well known world names. The secret being required is there to protect against such attacks as the secret value is unpredictable, and therefore only code running with the proper permissions on the website itself would be able to know the secret value required to cause content deletion.

You can obtain the secret key associated with a specific world for your specific user account by going to “My Table” and click the “Select Data Files to Delete” and selecting the world you want to delete.
The secret is the same value that I use to request confirmation from users through the website’s UI before deleting their content.

You can also obtain it programmatically by using the /api/data/worlds API endpoint using a GET method, which will list all of your available worlds as well as their associate secret value.

Putting it all together

This topic was created to show you, system and module developers, how you could write a cron job to automatically reset your demo world. While it delves into the deep end of the API system available (and only touches the tip of the iceberg of the undocumented APIs), here’s what you really need to do to reset your demo games :

  • Generate your API Key with the read-data, write-data and manage-games permissions
  • Periodically try to idle the server, if it returns success: true it means the server is not in use and it is safe to reset it. If it’s in use, you can decide to postpone the reset, or go ahead and do it anyway, it’s up to you.
  • Use the Stop server API to stop the server, because even if it’s idle/unused, you don’t want someone accessing it while you’re deleting/uploading the new world as you need Foundry to be stopped otherwise the database gets cached in memory and your file changes get overwritten by Foundry itself
  • If your demo world was made available through the Bazaar, use the worlds/install API to install it via its name
  • If your world isn’t in the Bazaar, first delete the world using your known secret value, then use the zip import to re-upload the world to a pristine state
  • You can now start your server again, but beware, if you use the Start API, the server not only will be immediately started (even if potentially not needed) but it will start in Foundry’s setup screen (which will yield an error of “This game is currently being configured” to any user who tries to access it), so it is instead recommended to use the game/idle API and specify the world in its arguments so the game is idled and starts directly into your demo world.

Do all of this using a script and put that in a cron job to run daily (or hourly or whatever) and you got yourself a demo server that is always ‘clean’.

Hopefully, it’s not so bad! :slight_smile:
Thoughts and suggestions are welcome!
Thanks for reading!

1 Like

If you aren’t familiar with (or are uncomfortable with) JavaScript you could, as mentioned, adapt this to curl. The below is a demonstration of how to do this. If you want to use the responses you will need something to parse them. Most systems will have access to a package install of ‘jq’ via command line (osx: brew install jq, ubuntu: sudo apt-get install jq, windows: choco install jq)

Basic CURL:

curl -H "Content-Type: application/json" -H "Access-Key: key" \
    -X POST https://forge-vtt.com/api/game/start 
curl -H "Content-Type: application/json" -H "Access-Key: key" \
   -X POST https://forge-vtt.com/api/game/idle`

Replace the word key with your key generated in Getting a new API Key in the tutorial. While you don’t strictly need the content-type set here it’s good practice whenever you use an API call and especially when you send data.

Basic CURL Storyteller:

curl -H "Content-Type: application/json" -H "Access-Key: key" \
   -X POST https://forge-vtt.com/api/game/idle \
   -d '{"game": "game-slug"}'

More advanced CURL+JQ examples
To get a response and parse it for scripts you can pipe the result to jq:

curl -H "Content-Type: application/json" -H "Access-Key: key" \
   -X POST https://forge-vtt.com/api/game/idle \
   -d '{"game": "game-slug"}' | jq "."

This would show you:

{
    "success" : {
        "result" : false
    }
}

To navigate the json objects and attribute pairs with JQ you use . (dot) referenced syntax for the name of the object or attribute key, so in this case: .success.result.

Additionally, you will want to use the --raw-output parameter with JQ to get the value of the attribute.

curl -H "Content-Type: application/json" -H "Access-Key: key" \
     -X POST https://forge-vtt.com/api/game/idle \ 
     -d '{"game": "game-slug"}' |  jq --raw-output ".success.result"

JQ fails silently by default, so this will return the string true or false based on the attribute value, if it exists, or blank if either .success or .result are missing. This is enough to get you writing simple batch scripts on the rest of the API to use in your cron jobs.

Please note, if you are using Windows then your powershell has an alias for curl that uses the powershell command Invoke-WebRequest, the syntax is slightly different (untested since I don’t use windows)

Invoke-WebRequest -ContentType application/json \
    -Headers @{"Access-Key" = "key"} -Method POST \ 
    -Body '{"game": "game-slug"}' -Uri https://forge-vtt.com/api/game/idle
1 Like