# Pterodactyl 1.x
* [Install the panel](./panel/getting-started.mdx)
* [Client API](./api/client-general/get-list-servers.mdx)
Use the version selector to switch to the Pterodactyl 2.0 documentation.
# Pterodactyl 2.0
Pterodactyl 2.0 is not released yet. These pages describe the development version and may change before release.
This is the documentation for Pterodactyl Panel 2.0. If you run 1.x, switch to the 1.x documentation with the version selector.
## Project Information [#project-information]
* [Introduction](./project/index.mdx) explains what Pterodactyl is and which games it supports.
* [How Pterodactyl Works](./project/how-it-works.mdx) explains the parts of Pterodactyl and the terms these pages use.
## Panel [#panel]
* [Getting Started](./panel/getting-started.mdx) installs the Panel on your own server.
* [Docker Deployment](./panel/docker.mdx) runs the Panel from its Docker image instead.
* [Webserver Configuration](./panel/webserver-configuration.mdx) sets up NGINX, Apache, or Caddy.
* [Updating the Panel](./panel/updating.mdx) updates the Panel to a newer 2.x release.
## Wings [#wings]
* [Installing Wings](./wings/installing.mdx) sets up a node to run your game servers.
* [Upgrading Wings](./wings/upgrading.mdx) updates Wings to a new version.
## Extensions [#extensions]
* [Extensions](./extensions/index.mdx) explains how to install and manage extensions.
* [Building Extensions](./extensions/building.mdx) shows how to build your own.
## Upgrading From 1.x [#upgrading-from-1x]
* [Changes From 1.x](./upgrading/changes-from-v1.mdx) lists what differs in 2.0 and who it affects.
* [Upgrading From 1.x](./upgrading/upgrading-from-v1.mdx) walks through the upgrade step by step.
Do not use the 1.x installation or update steps, or `php artisan p:upgrade`, to install 2.0.
## Community Guides [#community-guides]
* [Creating SSL Certificates](./guides/tutorials/ssl-certificates.mdx) gets free certificates for the Panel and Wings.
* [Setting Up MySQL](./guides/tutorials/mysql-setup.mdx) adds database hosts so servers can have their own databases.
* [Creating a Custom Egg](./guides/egg-creation/creating-custom-egg.mdx) shows how to import and build eggs.
* [Themes](./guides/customization/themes.mdx) changes the Panel's colors without rebuilding it.
## API Reference [#api-reference]
Start with the [API Overview](./api/index.mdx). It explains the three APIs and how to authenticate.
# API Overview
## Introduction [#introduction]
The Panel has two APIs. Choose the one that matches what you want to do:
| API | Path | Use it to |
| ----------- | ------------------ | ------------------------------------------------------------------------------- |
| Client | `/api/client` | Manage your account and the servers you have access to. |
| Application | `/api/application` | Manage users, nodes, locations, and servers, for example from a billing system. |
The `v1` in this documentation's address is not part of the API path.
## Authentication [#authentication]
Send your API key in the `Authorization` header of every request:
```http
Authorization: Bearer YOUR_API_KEY
```
Each API uses its own keys:
| API | Key | Created in |
| ----------- | ------------------------------------------------------ | ---------------------------------------------- |
| Client | A Client API key. It can only do what its user can do. | **Account → API Credentials** (`/account/api`) |
| Application | An Application API key. | **Admin → Application API** (`/admin/api`) |
Responses are JSON.
## Where to Start [#where-to-start]
* [List your servers](./client-general/get-list-servers.mdx) with the Client API.
* [List users](./application-users/get-list-users.mdx) with the Application API.
If you are upgrading to Pterodactyl 2.0, read [Changes From 1.x](../../v2/upgrading/changes-from-v1.mdx). The nest endpoints have been removed.
# Additional Configuration
## Backups [#backups]
Pterodactyl Panel allows users to create backups of their servers. In order to create backups, a backup storage method has to be configured.
When changing Pterodactyl Panel's backup storage method, users may still download or delete existing backups from the prior storage driver. In the instance of migrating from S3 to local backups, S3 credentials must remain configured after switching to the local backup storage method.
### Using Local Backups [#using-local-backups]
By default, Pterodactyl Panel uses local storage via Wings for backups. That said, this method of backup storage can be explicitly set with the following configuration in the `.env` file:
```bash
# Sets your panel to use local storage via Wings for backups
APP_BACKUP_DRIVER=wings
```
Do note that, when using local storage via Wings, the destination for backups is set in Wings' `config.yml` with the following setting key:
```yml
system:
backup_directory: /path/to/backup/storage
```
### Using S3 Backups [#using-s3-backups]
AWS S3 (or compatible storage) can be used to store remote or cloud-based backups. The following configuration options have to be set in the `.env` file or as environment variables in order to enable it:
```bash
# Sets your panel to use s3 for backups
APP_BACKUP_DRIVER=s3
# Info to actually use s3
AWS_DEFAULT_REGION=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_BACKUPS_BUCKET=
AWS_ENDPOINT=
```
For some configurations, you might have to change your S3 URL from `bucket.domain.com` to `domain.com/bucket`. To accomplish this, add `AWS_USE_PATH_STYLE_ENDPOINT=true` to your `.env` file.
#### Multipart Upload [#multipart-upload]
The S3 backup is using the S3 multipart upload capabilities. In rare situations, you might want to adjust the size of a single part or the lifespan of the generated pre-signed URLs. The default part size is 5GB, and the default pre-signed URL lifespan is 60 minutes.
You can configure the maximal part size using the `BACKUP_MAX_PART_SIZE` environment variable. You must specify the size in bytes. To define the pre-signed URL lifespan, use the `BACKUP_PRESIGNED_URL_LIFESPAN` variable. The expected unit is minutes.
The following `.env` snippet configures 1GB parts and uses 120 minutes as the pre-signed URL lifespan:
```bash
BACKUP_MAX_PART_SIZE=1073741824
BACKUP_PRESIGNED_URL_LIFESPAN=120
```
#### Storage Class [#storage-class]
Should you need to specify a storage class, use the `AWS_BACKUPS_STORAGE_CLASS` environment variable. Default option is `STANDARD` (S3 Standard).
The following `.env` snippet sets the class to `STANDARD_IA` (this is an example).
```bash
# STANDARD_IA is an example.
AWS_BACKUPS_STORAGE_CLASS=STANDARD_IA
```
## Reverse Proxy Setup [#reverse-proxy-setup]
When running Pterodactyl behind a reverse proxy, such as [Cloudflare's Flexible SSL](https://support.cloudflare.com/hc/en-us/articles/200170416-What-do-the-SSL-options-mean-)
or Nginx/Apache/Caddy, etc., you will need to make a quick modification to the Panel to ensure things continue to work as expected. By default, when using these reverse proxies,
your Panel will not correctly handle requests. You'll most likely be unable to login or see security warnings in your browser console as it attempts to load insecure assets.
This is because the internal logic the Panel uses to determine how links should be generated thinks it is running over HTTP and not over HTTPS.
You will need to edit the `.env` file in the Panel's root directory to contain `TRUSTED_PROXIES=*` at minimum. We highly suggest providing a specific IP address
(or comma-separated list of IPs) rather than allowing `*`. For example, if your proxy is running on the same machine as the server,
the chances are that something like `TRUSTED_PROXIES=127.0.0.1` will work for you.
### NGINX Specific Configuration [#nginx-specific-configuration]
For Pterodactyl to properly respond to an NGINX reverse proxy, the NGINX `location` config must contain the following lines:
```nginx
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering off;
proxy_request_buffering off;
```
### Cloudflare Specific Configuration [#cloudflare-specific-configuration]
If you're using Cloudflare's Flexible SSL you should set `TRUSTED_PROXIES` to contain [their IP addresses](https://www.cloudflare.com/ips/).
Below is an example of how to set this.
```text
TRUSTED_PROXIES=173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22
```
## reCAPTCHA [#recaptcha]
The Panel uses invisible reCAPTCHA to secure the login page from brute-force attacks. If the login attempt is considered suspicious, users may be required to perform a reCAPTCHA challenge.
### Configuring reCAPTCHA [#configuring-recaptcha]
While we provide a global Site Key and Secret Key by default, we highly recommend changing it for your own setup.
You can generate your own keys in the [reCAPTCHA Admin Console](https://www.google.com/recaptcha/admin).
The keys can then be applied using the Settings in the admin panel. The reCAPTCHA settings can be found on the **Advanced** tab.
### Disabling reCAPTCHA [#disabling-recaptcha]
We do not recommend disabling reCAPTCHA. It is a security mechanism that makes it harder to perform brute-force attacks on user accounts.
If users have trouble logging in, or your Panel isn't exposed to the internet, it can make sense to disable reCAPTCHA.
reCAPTCHA can easily be disabled using the admin panel. In the Settings, select the **Advanced** tab and set the **Status** of reCAPTCHA to **disabled**.
#### Editing your database [#editing-your-database]
If you cannot access your panel, you can modify the database directly using the following commands.
```sql
# If using MariaDB (v11.0.0+)
mariadb -u root -p
# If using MySQL
mysql -u root -p
```
```sql
INSERT INTO panel.settings (`key`, value) VALUES ('settings::recaptcha:enabled', 'false')
ON DUPLICATE KEY UPDATE value = 'false';
```
## 2FA [#2fa]
If possible you should use the panel to update your 2FA settings. If you can't access your panel for what ever reason you can use the following steps.
### Disable 2FA requirement [#disable-2fa-requirement]
```sql
# If using MariaDB (v11.0.0+)
mariadb -u root -p
# If using MySQL
mysql -u root -p
```
```sql
INSERT INTO panel.settings (`key`, value) VALUES ('settings::pterodactyl:auth:2fa_required', 0)
ON DUPLICATE KEY UPDATE value = 0;
```
### Disable 2FA for a specific user [#disable-2fa-for-a-specific-user]
Run the following command in your `/var/www/pterodactyl` directory.
```bash
php artisan p:user:disable2fa
```
## Telemetry [#telemetry]
Since 1.11, the Panel collects anonymous metrics about the Panel and all connected nodes.
This feature is enabled by default, but can be disabled.
The data collected by this feature is not sold or used for advertising purposes. Aggregate statistics
may be made public or shared with third-parties for the purposes of improving the software.
### How does it work? [#how-does-it-work]
The Telemetry system works by first generating a random UUIDv4 identifier for the Panel installation.
This identifier is stored in the database so people load-balancing multiple Panel instances can still
have a unique identifier. This identifier is then sent to a remote server, along the associated
telemetry data. The telemetry data is collected every 24 hours, there is no ongoing collection
or local storage of the telemetry data, we collect the data right before we send it to the remote
server.
Currently, all telemetry collection logic is handled by the [TelemetryCollectionService](https://github.com/pterodactyl/panel/blob/develop/app/Services/Telemetry/TelemetryCollectionService.php#L53)
on the panel. This service is responsible for collecting all the data that is sent to the remote
server.
### What data is collected? [#what-data-is-collected]
If you wish to see the full data that is collected, please look at the TelemetryCollectionService
(as linked above), or use the `php artisan p:telemetry` command to view the exact data that will
be sent to the remote server.
As of 2022-12-12, the data collected consists of:
* Unique identifier for the Panel
* Version of the Panel
* PHP version
* Backup storage driver (S3, Local, etc.)
* Cache driver (Redis, Memcached, etc.)
* Database driver and version (MySQL, MariaDB, PostgreSQL, etc.)
* Resources
* Allocations
* Total number
* Total number of used allocations (assigned to a server)
* Backups
* Total number
* Sum of the total amount of bytes stored by backups
* Eggs
* Total number
* ~~Map of egg UUIDs to the number of servers using that egg~~ (removed in 1.11.2)
* Locations
* Total number
* Mounts
* Total number
* Nests
* Total number
* ~~Map of nest UUIDs to the number of servers using eggs in that nest~~ (removed in 1.11.2)
* Nodes
* Total number
* Servers
* Total number
* Number of servers that are suspended
* Users
* Total number
* Number of users that are admins
* Nodes
* Node UUID
* Version of Wings on the node
* Docker
* Version
* Cgroups
* Driver
* Version
* Containers
* Total
* Running
* Paused
* Stopped
* Storage
* Driver
* Filesystem
* runc
* Version
* System
* Architecture (`amd64`, `arm64`, etc.)
* CPU Threads
* Memory Bytes
* Kernel Version
* Operating System (Debian, Fedora, RHEL, Ubuntu, etc.)
* Operating System Type (bsd, linux, windows, etc.)
### How is the data stored? [#how-is-the-data-stored]
Currently, the data is stored with Cloudflare, we ingest all telemetry data with a Worker which does
basic processing such as validation and then inserts it into Cloudflare D1. Right now, there is not
an API or visualization for any of the data collected, and it can only be manually queried. Only
Matthew is able to query the data at this time, but we are working on alternatives to make this data
more accessible.
### Why? [#why]
The primary reason for collecting this data is to help us make better decisions about the future of
this software. With the release of 1.11, the minimum PHP version requirement jumped from 7.4 to 8.0,
however, we wanted to add a feature that required PHP 8.1 which would've made the version requirement
jump larger and potentially cause issues for some users. By collecting this data, we can hopefully
have more insight to how changes like this will affect the community and make better decisions in the
future. This is especially important for information like the architecture, kernel version, and
operating system nodes are using. For example, we want to utilize a feature that is only present in
some filesystems, but we have no idea how many people are using those filesystems, so we cannot
determine if it's worth the effort to implement.
Some of the data is not as useful for making decisions, but is still useful for us to know.
For example, have you ever wondered how many Panel instances there are? How many servers are being
ran across all of those instances? How many users are using the Panel? How many of those users are
admins? How many servers are using a specific egg? How many servers are using a specific nest?
All of these questions can be answered by the data we collect, and can help us and the community
better understand how the software is being used.
If you have any questions about the data we collect, please feel free to reach out to us on Discord.
Our goal is to be as transparent as possible, and we want to make sure that the community understands
what we are doing and why.
### Enabling Telemetry [#enabling-telemetry]
Telemetry is enabled by default, if you want to enable it after disabling it, edit your `.env` file
and either remove the `PTERODACTYL_TELEMETRY_ENABLED` line, or set it to `true`.
```text
PTERODACTYL_TELEMETRY_ENABLED=true
```
You may also use the `php artisan p:environment:setup` command to enable telemetry, optionally with
the `--telemetry` flag for a non-interactive setup.
### Disabling Telemetry [#disabling-telemetry]
To disable telemetry, edit your `.env` file and set `PTERODACTYL_TELEMETRY_ENABLED` to `false`.
```text
PTERODACTYL_TELEMETRY_ENABLED=false
```
You may also use the `php artisan p:environment:setup` command to disable telemetry, optionally with
the `--telemetry=false` flag for a non-interactive setup.
# Getting Started
Pterodactyl Panel is designed to run on your own web server. You will need to have root access to your server in order to run and use this panel.
You are expected to understand how to read documentation to use this Panel. We have spent many hours detailing how to install or upgrade our
software; take some time and read rather than copy and pasting and then complaining when things do not work. This panel does
not exist as a drag-and-drop service to run your servers. It is a highly complex system requiring multiple dependencies and
administrators willing to spend some time learning how to use it. **If you expect to be able to install this with no understanding
of basic linux system administration you should stop and turn around now.**
[WISP](https://wisp.gg) is a Pterodactyl powered SaaS suitable for enterprise and personal use. Offering all the features without the setup hassle, and fully compatible with Pterodactyl eggs. Comparable to MultiCraft or TCAdmin while offering new and unique features. Click here to [learn more](https://wisp.gg/features).
## Picking a Server OS [#picking-a-server-os]
Pterodactyl runs on a wide range of operating systems, so pick whichever you are most comfortable using.
Running Wings under LXC or OpenVZ containers is possible as long as the virtualization layer supports Docker. Most modern
providers with LXC/OpenVZ 7+ and nested virtualization enabled will work without issues. If you run into problems, ensure
that your provider has enabled the necessary kernel features for Docker.
| Operating System | Version | Supported | Notes |
| ---------------------------------- | ------- | :-------: | ----------------------------------------------------------- |
| **Ubuntu** | 22.04 | ✅ | Requires additional repositories for PHP |
| | 24.04 | ✅ | MariaDB can be installed without the repo setup script. |
| **RHEL / Rocky Linux / AlmaLinux** | 8 | ✅ | Extra repos are required. |
| | 9 | ✅ | |
| **Debian** | 11 | ✅ | [Debian Dependencies](/v1/guides/panel-installation/debian) |
| | 12 | ✅ | [Debian Dependencies](/v1/guides/panel-installation/debian) |
| | 13 | ✅ | [Debian Dependencies](/v1/guides/panel-installation/debian) |
## Dependencies [#dependencies]
* PHP `8.2` or `8.3` (recommended) with the following extensions: `cli`, `openssl`, `gd`, `mysql`, `PDO`, `mbstring`, `tokenizer`, `bcmath`, `xml` or `dom`, `curl`, `zip`, and `fpm` if you are planning to use NGINX.
* MySQL `5.7.22` and higher (MySQL `8` recommended) **or** MariaDB `10.2` and higher.
* Redis (`redis-server`)
* A webserver (Apache, NGINX, Caddy, etc.)
* `curl`
* `tar`
* `unzip`
* `git`
* `composer` v2
### Example Dependency Installation [#example-dependency-installation]
The commands below are simply an example of how you might install these dependencies. Please consult with your
operating system's package manager to determine the correct packages to install.
```bash
# Add "add-apt-repository" command
apt -y install software-properties-common curl apt-transport-https ca-certificates gnupg
# Add additional repositories for PHP (Ubuntu 22.04)
LC_ALL=C.UTF-8 add-apt-repository -y ppa:ondrej/php
# Add Redis official APT repository
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
# Update repositories list
apt update
# Install Dependencies
apt -y install php8.3 php8.3-{common,cli,gd,mysql,mbstring,bcmath,xml,fpm,curl,zip} mariadb-server nginx tar unzip git redis-server
```
### Installing Composer [#installing-composer]
Composer is a dependency manager for PHP that allows us to ship everything you'll need code wise to operate the Panel. You'll
need composer installed before continuing in this process.
```bash
curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer
```
## Download Files [#download-files]
The first step in this process is to create the folder where the panel will live and then move ourselves into that
newly created folder. Below is an example of how to perform this operation.
```bash
mkdir -p /var/www/pterodactyl
cd /var/www/pterodactyl
```
Once you have created a new directory for the Panel and moved into it you'll need to download the Panel files. This
is as simple as using `curl` to download our pre-packaged content. Once it is downloaded you'll need to unpack the archive
and then set the correct permissions on the `storage/` and `bootstrap/cache/` directories. These directories
allow us to store files as well as keep a speedy cache available to reduce load times.
```bash
curl -Lo panel.tar.gz https://github.com/pterodactyl/panel/releases/latest/download/panel.tar.gz
tar -xzvf panel.tar.gz
chmod -R 755 storage/* bootstrap/cache/
```
## Installation [#installation]
Now that all of the files have been downloaded we need to configure some core aspects of the Panel.
You will need a database setup and a user with the correct permissions created for that database before
continuing any further. See below to create a user and database for your Pterodactyl panel quickly. To find more detailed information
please have a look at [Setting up MySQL](/v1/guides/tutorials/mysql-setup).
```sql
# If using MariaDB (v11.0.0+) (This is the default when installing Pterodactyl by following the documentation.)
mariadb -u root -p
# If using MySQL
mysql -u root -p
```
```sql
# Remember to change 'yourPassword' below to be a unique password
CREATE USER 'pterodactyl'@'127.0.0.1' IDENTIFIED BY 'yourPassword';
CREATE DATABASE panel;
GRANT ALL PRIVILEGES ON panel.* TO 'pterodactyl'@'127.0.0.1' WITH GRANT OPTION;
exit
```
First we will copy over our default environment settings file, install core dependencies, and then generate a
new application encryption key.
```bash
cp .env.example .env
COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader
# Only run the command below if you are installing this Panel for
# the first time and do not have any Pterodactyl Panel data in the database.
php artisan key:generate --force
```
Back up your encryption key (`APP_KEY` in the `.env` file). It is used as an encryption key for all data that needs to be stored securely (e.g. API keys).
Store it somewhere safe - not just on your server. If you lose it, all encrypted data is irrecoverable, even with database backups.
To grab your `APP_KEY`, open a terminal and run the following in your panel directory:
```bash
grep APP_KEY /var/www/pterodactyl/.env
```
You should see something like:
```text
APP_KEY=base64:YOUR_LONG_RANDOM_STRING
```
Copy that entire line and save it somewhere secure:
* A password manager
* An encrypted file on your local machine
* A secure USB drive
* A trusted cloud vault
Do not keep it only on the server. If you lose this key, your encrypted data is permanently unrecoverable.
### Environment Configuration [#environment-configuration]
Pterodactyl's core environment is easily configured using a few different CLI commands built into the app. This step
will cover setting up things such as sessions, caching, database credentials, and email sending.
```bash
php artisan p:environment:setup
php artisan p:environment:database
# To use PHP's internal mail sending (not recommended), select "mail". To use a
# custom SMTP server, select "smtp".
php artisan p:environment:mail
```
### Database Setup [#database-setup]
Now we need to setup all of the base data for the Panel in the database you created earlier. **The command below
may take some time to run depending on your machine. Please *DO NOT* exit the process until it is completed!** This
command will setup the database tables and then add all of the Nests & Eggs that power Pterodactyl.
```bash
php artisan migrate --seed --force
```
### Add The First User [#add-the-first-user]
You'll then need to create an administrative user so that you can log into the panel. To do so, run the command below.
At this time passwords **must** meet the following requirements: 8 characters, mixed case, at least one number.
```bash
php artisan p:user:make
```
### Set Permissions [#set-permissions]
The last step in the installation process is to set the correct permissions on the Panel files so that the webserver can
use them correctly.
```bash
# If using NGINX, Apache or Caddy (not on RHEL / Rocky Linux / AlmaLinux)
chown -R www-data:www-data /var/www/pterodactyl/*
# If using NGINX on RHEL / Rocky Linux / AlmaLinux
chown -R nginx:nginx /var/www/pterodactyl/*
# If using Apache on RHEL / Rocky Linux / AlmaLinux
chown -R apache:apache /var/www/pterodactyl/*
```
## Queue Listeners [#queue-listeners]
We make use of queues to make the application faster and handle sending emails and other actions in the background.
You will need to setup the queue worker for these actions to be processed.
### Crontab Configuration [#crontab-configuration]
The first thing we need to do is create a new cronjob that runs every minute to process specific Pterodactyl tasks, such
as session cleanup and sending scheduled tasks to daemons. You'll want to open your crontab using `sudo crontab -e` and
then paste the line below.
```bash
* * * * * php /var/www/pterodactyl/artisan schedule:run >> /dev/null 2>&1
```
### Create Queue Worker [#create-queue-worker]
Next you need to create a new systemd worker to keep our queue process running in the background. This queue is responsible
for sending emails and handling many other background tasks for Pterodactyl.
Create a file called `pteroq.service` in `/etc/systemd/system` with the contents below.
```text
# Pterodactyl Queue Worker File
# ----------------------------------
[Unit]
Description=Pterodactyl Queue Worker
After=redis-server.service
[Service]
# On some systems the user and group might be different.
# Some systems use `apache` or `nginx` as the user and group.
User=www-data
Group=www-data
Restart=always
ExecStart=/usr/bin/php /var/www/pterodactyl/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3
StartLimitInterval=180
StartLimitBurst=30
RestartSec=5s
[Install]
WantedBy=multi-user.target
```
If you are using RHEL, Rocky Linux, or AlmaLinux, you will need to replace `redis-server.service` with `redis.service` at the `After=` line in order to ensure `redis` starts before the queue worker.
If you are not using `redis` for anything you should remove the `After=` line, otherwise you will encounter errors
when the service starts.
If you are using redis for your system, you will want to make sure to enable that it will start on boot. You can do that by running the following command:
```bash
sudo systemctl enable --now redis-server
```
Finally, enable the service and set it to boot on machine start.
```bash
sudo systemctl enable --now pteroq.service
```
### Telemetry [#telemetry]
Since 1.11, Pterodactyl will collect anonymous telemetry to help us better understand how the
software is being used. To learn more about this feature and to opt-out, please see our [Telemetry](/v1/panel/additional-configuration#telemetry)
documentation. Make sure to continue with the rest of the installation process.
# Troubleshooting
## Reading Error Logs [#reading-error-logs]
If you ever encounter an unexpected error with the Panel the first thing you will likely be asked for is the logs.
To retrieve these, simply execute the command below which will output the last 100 lines of the Panel's log file.
```bash
tail -n 100 /var/www/pterodactyl/storage/logs/laravel-$(date +%F).log
```
### Parsing the Error [#parsing-the-error]
When you run the command above, you'll probably be hit with a huge wall of text that might scare you. Fear not,
this is simply a stacktrace leading to the cause of the error, and you can actually ignore almost all of it when
looking for the cause of the error. Lets take a look at some example output below, which has been truncated to
make this easier to follow with.
```
#70 /srv/www/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(116): Illuminate\Foundation\Http\Kernel->sendRequestThroughRouter(Object(Illuminate\Http\Request))
#71 /srv/www/public/index.php(53): Illuminate\Foundation\Http\Kernel->handle(Object(Illuminate\Http\Request))
#72 {main}
[2018-07-19 00:50:24] local.ERROR: ErrorException: file_put_contents(/srv/www/storage/framework/views/c9c05d1357df1ce4ec8fc5df78c16c493b0d4f48.php): failed to open stream: Permission denied in /srv/www/vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php:122
Stack trace:
#0 [internal function]: Illuminate\Foundation\Bootstrap\HandleExceptions->handleError(2, 'file_put_conten...', '/srv/www/vendor...', 122, Array)
#1 /srv/www/vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php(122): file_put_contents('/srv/www/storag...', 's...', 0)
#2 /srv/www/vendor/laravel/framework/src/Illuminate/View/Compilers/BladeCompiler.php(122): Illuminate\Filesystem\Filesystem->put('/srv/www/storag...', 's...')
#3 /srv/www/vendor/laravel/framework/src/Illuminate/View/Engines/CompilerEngine.php(51): Illuminate\View\Compilers\BladeCompiler->compile('/srv/www/resour...')
#4 /srv/www/vendor/laravel/framework/src/Illuminate/View/View.php(142): Illuminate\View\Engines\CompilerEngine->get('/srv/www/resour...', Array)
#5 /srv/www/vendor/laravel/framework/src/Illuminate/View/View.php(125): Illuminate\View\View->getContents()
```
The first thing you'll want to do is follow the chain of numbers *up* until you find `#0`, this will be the function that
triggered the exception. Right above line 0 you will see a line that has the date and time in brackets, `[2018-07-19 00:50:24]`
above for example. This line will be the human readable exception that you can use to understand what went wrong.
### Understanding the Error [#understanding-the-error]
In the example above we can see that the actual error was:
```
local.ERROR: ErrorException: file_put_contents(...): failed to open stream: Permission denied in /srv/www/vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php:122
```
From this error we can determine that there was an error performing a [file\_put\_contents()](http://php.net/manual/en/function.file-put-contents.php) call, and the error was
that we couldn't open the file because permissions were denied. Its okay if you don't understand the error at all, but
it does help you get faster support if you're able to provide these logs, and at least find the source of the error.
Sometimes the errors are pretty straightforward and will tell you exactly what went wrong, such as a `ConnectionException`
being thrown when the Panel can't connect to the Daemon.
### Utilizing GREP [#utilizing-grep]
If you're trying to go through a bunch of errors quickly, you can use the command below which will limit the results returned to only
be the actual error lines, without all of the stack traces.
```bash
tail -n 1000 /var/www/pterodactyl/storage/logs/laravel-$(date +%F).log | grep "\[$(date +%Y)"
```
## Cannot Connect to Server Errors [#cannot-connect-to-server-errors]
### Basic Debugging Steps [#basic-debugging-steps]
* Check that Wings is running, and not reporting errors. Use `systemctl status wings` to check the current status of
the process.
* Check your browser's console by pressing `Ctrl + Shift + J` (in Chrome) or `Cmd + Alt + I` (in Safari). If there is
a red error in it, chances are that it will narrow down the potential problem.
* Make sure Wings is properly installed and the active configuration matches the configuration shown under
`Admin -> Node -> Configuration` in the Panel.
* Check that the Wings ports are open on your firewall. Wings uses ports `8080` or `8443` for HTTP(s) traffic,
and `2022` for SFTP traffic.
* Ensure you have AdBlock disabled or whitelisted for your Panel and Wings domains.
* Check that the Panel can reach Wings using the domain that is configured on the Panel. Run `curl
https://domain.com:8080` on the Panel server and ensure that it can successfully connect to Wings.
* Ensure that you are using the correct HTTP scheme for your Panel and Wings. If the Panel is running over HTTPS
Wings will also need to be running on HTTPS.
* If using HTTPS for Wings, make sure that the certificates have not expired.
### More Advanced Debugging Steps [#more-advanced-debugging-steps]
* Stop Wings and run `wings --debug` to see if there are any errors being output. If so, try resolving them manually,
or reach out on [Discord](https://discord.gg/pterodactyl) for more assistance.
* Check your DNS and ensure that the response you receive is the one you expect using a tool such as `nslookup` or `dig`.
* If you use CloudFlare make sure that the orange cloud is disabled for your Wings or Panel `A` records.
* Make sure when using Wings behind a firewall — pfSense, OpenSwitch, etc. — that the correct NAT settings to access
the Wing's ports from the outside network are setup.
* If nothing is working so far, check your own DNS settings and consider switching DNS servers.
* When running the Panel and Wings on one server it can sometimes help if to add an entry in `/etc/hosts` that directs
the public IP back to the server. Sometimes the reverse path is also needed, so you may need to add an entry to your
servers `/etc/hosts` file that points the Panel's domain to the correct IP.
* When running Wings and the Panel on separate VM's using the same adapter make sure the VM's can connect to each
other. Promiscuous mode might be needed.
## Invalid MAC Exception [#invalid-mac-exception]
This error should never happen if you correctly follow our installation and upgrade guides. The only time we have
ever seen this error occur is when you blindly restore the Panel database from a backup and try to use a fresh
installation of the Panel.
When restoring backups you should *always* restore the `.env` file!
Sometimes when using the Panel you'll unexpectedly encounter a broken page, and upon checking the logs you'll see
an exception mentioning an invalid MAC when decrypting. This error is caused by mismatched `APP_KEY`s in your `.env` file
when the data was encrypted versus decrypted.
If you are seeing this error the only solution is to restore the `APP_KEY` from your `.env` file. If you have lost that
original key there is no way to recover the lost data.
## SELinux Issues [#selinux-issues]
On systems with SELinux installed you might encounter unexpected errors when running redis or attempting to connect
to the daemon to perform actions. These issues can generally be resolved by executing the commands below to allow
these programs to work with SELinux.
### Redis Permissions Errors [#redis-permissions-errors]
```bash
audit2allow -a -M redis_t
semodule -i redis_t.pp
```
### Wings Connection Errors [#wings-connection-errors]
```bash
audit2allow -a -M http_port_t
semodule -i http_port_t.pp
```
## Containers don't have internet? Probably a DNS issue! [#containers-dont-have-internet-probably-a-dns-issue]
Now that Wings has run successfully and you have gotten the green heart on your Nodes page, the wings config at '/etc/pterodactyl/config.yml' will have new values.
One of those values is DNS, which by default will be 1.1.1.1 and 1.0.0.1
If you are using a host that blocks Cloudflare DNS, you will have to use different DNS Servers; typically the same ones your host system is using.
You can view what DNS Servers your host uses through a number of ways depending on how your operating system handles networking. If one of these doesn't work, try another one.
```bash
# Network Manager (This will show both your IPV4 DNS and IPV6 DNS Servers in case you want to add the IPV6 DNS Server(s) from your host to your Wings Config as well.
nmcli -g ip4.dns,ip6.dns dev show
# Resolve-CTL (Newer Versions of Ubuntu)
resolvectl status
# Raw file locations that may have your host system's DNS Servers for various distributions
/etc/resolv.conf
/etc/network/interfaces
```
If this returns different DNS Servers than 1.1.1.1 and 1.0.0.1 you'll need to edit the wings 'config.yml' file to use the DNS servers that were returned from the command. If you see output that looks like an IPV6 address in addition to your IPV4 DNS Servers, make sure you put that in the IPV6 section and not the IPV4 section. To be clear, if you have to use different DNS Servers than the default, make sure to REMOVE 1.1.1.1 and 1.0.0.1 from the wings config; don't just add the new servers, replace the old servers.
## Schedule Troubleshooting [#schedule-troubleshooting]
* Check logs from your queue manager `journalctl -xeu pteroq`
* Restart pteroq `systemctl restart pteroq`
* Clear schedule cache `php /var/www/pterodactyl/artisan schedule:clear-cache`
* Check your php version `php -v` - [this page](/v1/panel/updating#panel-version-requirements) will tell you what versions of php are supported by what versions of the panel
* Check your crontab syntax using [https://crontab.guru](https://crontab.guru) - make sure it's what you intended
* Verify the problem is with the schedule and not with the tasks you have set up (Set the first task in your schedule to something you know prints a message in the console, ie. run `say test` in the console for a Minecraft server, if the text "test" shows up in the console successfully, set the first task to `say test` so you know if it runs
* Are your tasks off by a bit? Make sure you on the latest version of the panel? In version 1.11.5 there was a fix for schedules running at the wrong time. Alternatively, you may have the wrong timezone set. Make sure your timezones all match.
* System Timezone `timedatectl`
* Panel Timezone `nano /var/www/pterodactyl/.env`
* Wings Timezone (Passed to containers as the TZ environmental variable, unrelated to schedules but while you're checking timezones you may as well set this too) `nano /etc/pterodactyl/config.yml`
* Check your database where schedules are stored - MariaDB by default
* `systemctl status mariadb` - if it's not active, `journalctl -xeu mariadb`
* Check queue handler - Redis by default
* `systemctl status redis` - if it's not active, `journalctl -xeu redis` (On some distributions the service will be named `redis-server` instead)
* Check for panel errors `tail -n 150 /var/www/pterodactyl/storage/logs/laravel-$(date +%F).log | nc pteropaste.com 99`
## FirewallD issues [#firewalld-issues]
If you are on a RHEL/CentOS server with `firewalld` installed you may have broken DNS.
```bash
firewall-cmd --permanent --zone=trusted --change-interface=pterodactyl0
firewall-cmd --reload
```
Restart `docker` and `wings` after running these to be sure the rules are applied.
# Updating the Panel
This documentation covers the process for updating within the `1.x` series of releases. This means updating from
— for example — `1.11.x` to `1.12.x`.
Do not use these steps to upgrade to 2.0. They unpack the new version on top of your current files, which leaves a
2.0 Panel offline. For the same reason, do not run `php artisan p:upgrade` without `--release`. Once 2.0 is released,
it will download 2.0. To upgrade to 2.0, follow [Upgrading From 1.x](/v2/upgrading/upgrading-from-v1).
## Panel Version Requirements [#panel-version-requirements]
Each version of Pterodactyl Panel also has a corresponding minimum version of Wings that
is required for it to run. Please see the chart below for how these versions line up. In
most cases your base Wings version should match that of your Panel.
| Panel Version | Wings Version | Supported | PHP Versions |
| ------------- | ------------- | --------- | ----------------- |
| 1.0.x | 1.0.x | | 7.3, 7.4 |
| 1.1.x | 1.1.x | | 7.3, 7.4 |
| 1.2.x | 1.2.x | | 7.3, 7.4 |
| 1.3.x | 1.3.x | | 7.4, 8.0 |
| 1.4.x | 1.4.x | | 7.4, 8.0 |
| 1.5.x | 1.4.x | | 7.4, 8.0 |
| 1.6.x | 1.4.x | | 7.4, 8.0 |
| 1.7.x | 1.5.x | | 7.4, 8.0 |
| 1.8.x | 1.6.x | | 7.4, 8.0, 8.1 |
| 1.9.x | 1.6.x | | 7.4, 8.0, 8.1 |
| 1.10.x | 1.7.x | | 7.4, 8.0, 8.1 |
| 1.11.x | 1.11.x | | ~~8.1~~, 8.2, 8.3 |
| **1.12.x** | **1.12.x** | ✅ | 8.2, **8.3** |
There are no 1.8.x, 1.9.x, or 1.10.x releases of Wings.
## Update Dependencies [#update-dependencies]
* PHP `8.2`, or `8.3` (recommended)
* Composer `2.X`
**Before continuing**, please ensure that your system and web server configuration has been upgraded to at least PHP 8.2 by running `php -v` and Composer 2 by running `composer --version`. You
should see an output similar to the result below. If you do not see at least PHP 8.2 and Composer 2, you will need to upgrade by following
our [PHP Upgrade Guide](/v1/guides/configuration/php-upgrade) and return to this documentation afterward.
```shell
vagrant@pterodactyl:~/app$ php -v
PHP 8.3.30 (cli) (built: Dec 21 2022 10:32:13) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.30, Copyright (c) Zend Technologies
with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies
vagrant@pterodactyl:~/app$ composer --version
Composer version 2.8.12 2025-09-19 13:41:59
```
## Upgrade Steps [#upgrade-steps]
Follow the steps below to upgrade your Panel to the latest version.
### Enter Maintenance Mode [#enter-maintenance-mode]
Whenever you are performing an update you should be sure to place your Panel into maintenance mode. This will prevent
users from encountering unexpected errors and ensure everything can be updated before users encounter
potentially new features.
```bash
cd /var/www/pterodactyl
php artisan down
```
### Download the Update [#download-the-update]
The first step in the update process is to download the new panel files from GitHub. The command below will download
the release archive for the latest 1.x release (1.15.1 at the time of writing) and will automatically unpack the archive
into your current folder. Do not replace the version with `latest`: once 2.0 is published, `latest` points at 2.0.
```bash
curl -L https://github.com/pterodactyl/panel/releases/download/v1.15.1/panel.tar.gz | tar -xzv
```
Once all of the files are downloaded we need to set the correct permissions on the cache and storage directories to avoid
any webserver related errors.
```bash
chmod -R 755 storage/* bootstrap/cache
```
### Update Dependencies [#update-dependencies-1]
After you've downloaded all of the new files you will need to upgrade the core components of the panel. To do this,
simply run the commands below and follow any prompts.
```bash
composer install --no-dev --optimize-autoloader
```
### Clear Compiled Template Cache [#clear-compiled-template-cache]
You'll also want to clear the compiled template cache to ensure that new and modified templates show up correctly for
users.
```bash
php artisan view:clear
php artisan config:clear
```
### Database Updates [#database-updates]
You'll also need to update your database schema for the newest version of Pterodactyl. Running the command below
will update the schema and ensure the default eggs we ship are up to date (and add any new ones we might have). Just
remember, *never edit core eggs we ship*! They will be overwritten by this update process.
```bash
php artisan migrate --seed --force
```
### Set Permissions [#set-permissions]
The last step is to set the proper owner of the files to be the user that runs your webserver. In most cases this
is `www-data` but can vary from system to system — sometimes being `nginx`, `caddy`, `apache`, or even `nobody`.
```bash
# If using NGINX, Apache or Caddy (not on RHEL / Rocky Linux / AlmaLinux)
chown -R www-data:www-data /var/www/pterodactyl/*
# If using NGINX on RHEL / Rocky Linux / AlmaLinux
chown -R nginx:nginx /var/www/pterodactyl/*
# If using Apache on RHEL / Rocky Linux / AlmaLinux
chown -R apache:apache /var/www/pterodactyl/*
```
### Restarting Queue Workers [#restarting-queue-workers]
After *every* update you should restart the queue worker to ensure that the new code is loaded in and used.
```bash
php artisan queue:restart
```
### Exit Maintenance Mode [#exit-maintenance-mode]
Now that everything has been updated you need to exit maintenance mode so that the Panel can resume accepting
connections.
```bash
php artisan up
```
### Telemetry [#telemetry]
Since 1.11, Pterodactyl will collect anonymous telemetry to help us better understand how the
software is being used. To learn more about this feature and to opt-out, please see our [Telemetry](/v1/panel/additional-configuration#telemetry)
documentation. Remember to continue with the rest of the upgrade.
# Webserver Configuration
When using the SSL configuration you MUST create SSL certificates, otherwise your webserver will fail to start. See the [Creating SSL Certificates](/v1/guides/tutorials/ssl-certificates) documentation page to learn how to create these certificates before continuing.
If you are using Caddy With Automatic SSL, you do not have to create SSL certificates manually, Caddy will take care of it automatically.
First, remove the default NGINX configuration.
```bash
rm /etc/nginx/sites-enabled/default
```
Now, you should paste the contents of the file below, replacing `` with your domain name being used in a file called
`pterodactyl.conf` and place the file in `/etc/nginx/sites-available/`, or — if on RHEL, Rocky Linux, or AlmaLinux, `/etc/nginx/conf.d/`.
```nginx title="pterodactyl.conf" {4,11,26-27}
server {
# Replace the example with your domain name or IP address
listen 80;
server_name ;
return 301 https://$server_name$request_uri;
}
server {
# Replace the example with your domain name or IP address
listen 443 ssl http2;
server_name ;
root /var/www/pterodactyl/public;
index index.php;
access_log /var/log/nginx/pterodactyl.app-access.log;
error_log /var/log/nginx/pterodactyl.app-error.log error;
# allow larger file uploads and longer script runtimes
client_max_body_size 100m;
client_body_timeout 120s;
sendfile off;
# SSL Configuration - Replace the example with your domain
ssl_certificate /etc/letsencrypt/live//fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live//privkey.pem;
ssl_session_cache shared:SSL:10m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
ssl_prefer_server_ciphers on;
# See https://hstspreload.org/ before uncommenting the line below.
# add_header Strict-Transport-Security "max-age=15768000; preload;";
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;
add_header Content-Security-Policy "frame-ancestors 'self'";
add_header X-Frame-Options DENY;
add_header Referrer-Policy same-origin;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size=100M";
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_PROXY "";
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
include /etc/nginx/fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}
```
### Enabling Configuration [#enabling-configuration]
The final step is to enable your NGINX configuration and restart it.
```bash
# You do not need to symlink this file if you are using RHEL, Rocky Linux, or AlmaLinux.
sudo ln -s /etc/nginx/sites-available/pterodactyl.conf /etc/nginx/sites-enabled/pterodactyl.conf
# You need to restart nginx regardless of OS.
sudo systemctl restart nginx
```
First, remove the default NGINX configuration.
```bash
rm /etc/nginx/sites-enabled/default
```
Now, you should paste the contents of the file below, replacing `` with your domain name being used in a file called
`pterodactyl.conf` and place the file in `/etc/nginx/sites-available/`, or — if on RHEL, Rocky Linux, or AlmaLinux, `/etc/nginx/conf.d/`.
```nginx title="pterodactyl.conf" {4}
server {
# Replace the example with your domain name or IP address
listen 80;
server_name ;
root /var/www/pterodactyl/public;
index index.html index.htm index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log /var/log/nginx/pterodactyl.app-error.log error;
# allow larger file uploads and longer script runtimes
client_max_body_size 100m;
client_body_timeout 120s;
sendfile off;
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size=100M";
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_PROXY "";
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
}
location ~ /\.ht {
deny all;
}
}
```
### Enabling Configuration [#enabling-configuration-1]
The final step is to enable your NGINX configuration and restart it.
```bash
# You do not need to symlink this file if you are using RHEL, Rocky Linux, or AlmaLinux.
sudo ln -s /etc/nginx/sites-available/pterodactyl.conf /etc/nginx/sites-enabled/pterodactyl.conf
# You need to restart nginx regardless of OS.
sudo systemctl restart nginx
```
First, remove the default Apache configuration.
```bash
a2dissite 000-default.conf
```
Now, you should paste the contents of the file below, replacing `` with your domain name being used in a file called
`pterodactyl.conf` and place the file in `/etc/apache2/sites-available`, or — if on RHEL, Rocky Linux, or AlmaLinux, `/etc/httpd/conf.d/`.
Note: When using Apache, make sure you have the `libapache2-mod-php8.3` package installed or else PHP will not display on your webserver.
```apache title="pterodactyl.conf" {3,12,26-27}
# Replace the example with your domain name or IP address
ServerName
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L]
# Replace the example with your domain name or IP address
ServerName
DocumentRoot "/var/www/pterodactyl/public"
AllowEncodedSlashes On
php_value upload_max_filesize 100M
php_value post_max_size 100M
Require all granted
AllowOverride all
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live//fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live//privkey.pem
```
### Enabling Configuration [#enabling-configuration-2]
Once you've created the file above, simply run the commands below. If you are on RHEL, Rocky Linux, or AlmaLinux you do not need to run the commands
below! You only need to run `systemctl restart httpd`.
```bash
# You do not need to run any of these commands on RHEL, Rocky Linux, or AlmaLinux
sudo ln -s /etc/apache2/sites-available/pterodactyl.conf /etc/apache2/sites-enabled/pterodactyl.conf
sudo a2enmod rewrite
sudo a2enmod ssl
sudo systemctl restart apache2
```
First, remove the default Apache configuration.
```bash
a2dissite 000-default.conf
```
Now, you should paste the contents of the file below, replacing `` with your domain name being used in a file called
`pterodactyl.conf` and place the file in `/etc/apache2/sites-available`, or — if on RHEL, Rocky Linux, or AlmaLinux, `/etc/httpd/conf.d/`.
Note: When using Apache, make sure you have the `libapache2-mod-php8.3` package installed or else PHP will not display on your webserver.
```apache title="pterodactyl.conf" {3}
# Replace the example with your domain name or IP address
ServerName
DocumentRoot "/var/www/pterodactyl/public"
AllowEncodedSlashes On
php_value upload_max_filesize 100M
php_value post_max_size 100M
AllowOverride all
Require all granted
```
### Enabling Configuration [#enabling-configuration-3]
Once you've created the file above, simply run the commands below. If you are on RHEL, Rocky Linux, or AlmaLinux you do not need to run the commands
below! You only need to run `systemctl restart httpd`.
```bash
# You do not need to run any of these commands on RHEL, Rocky Linux, or AlmaLinux
sudo ln -s /etc/apache2/sites-available/pterodactyl.conf /etc/apache2/sites-enabled/pterodactyl.conf
sudo a2enmod rewrite
sudo systemctl restart apache2
```
Before adding our custom configuration, let's remove the default one. You can do it either by deleting the contents of config file or by deleting the config file completely and than creating a new one from scratch. The config file path is `/etc/caddy/Caddyfile`.
To delete the config file completely, run the following command:
```shell
rm /etc/caddy/Caddyfile
```
Then continue with an editor of your choice to write the config.
You should paste the contents of the file below, replacing `` with your domain name.
```shell title="Caddyfile" {10}
{
servers :443 {
timeouts {
read_body 120s
}
}
}
# Replace the example with your domain name or IP address
{
root * /var/www/pterodactyl/public
file_server
php_fastcgi unix//run/php/php8.3-fpm.sock {
root /var/www/pterodactyl/public
index index.php
env PHP_VALUE "upload_max_filesize = 100M
post_max_size = 100M"
env HTTP_PROXY ""
env HTTPS "on"
read_timeout 300s
dial_timeout 300s
write_timeout 300s
}
header Strict-Transport-Security "max-age=16768000; preload;"
header X-Content-Type-Options "nosniff"
header X-XSS-Protection "1; mode=block;"
header X-Robots-Tag "none"
header Content-Security-Policy "frame-ancestors 'self'"
header X-Frame-Options "DENY"
header Referrer-Policy "same-origin"
request_body {
max_size 100m
}
respond /.ht* 403
log {
output file /var/log/caddy/pterodactyl.log {
roll_size 100MiB
roll_keep_for 7d
}
level INFO
}
}
```
If you are using Cloudflare DNS in proxy mode, refer to [this tutorial](/v1/guides/tutorials/ssl-certificates#method-3-caddy-using-cloudflare-api), to see how to configure Caddy to use DNS challenge for obtaining SSL certificates.
### Enabling Configuration [#enabling-configuration-4]
The final step is to restart Caddy.
```bash
systemctl restart caddy
```
Before adding our custom configuration, let's remove the default one. You can do it either by deleting the contents of config file or by deleting the config file completely and than creating a new one from scratch. The config file path is `/etc/caddy/Caddyfile`.
To delete the config file completely, run the following command:
```shell
rm /etc/caddy/Caddyfile
```
Then continue with an editor of your choice to write the config.
You should paste the contents of the file below, replacing `` with your domain name.
The only two differences are that we have suffixed the `` with `:80` and in the global config at `servers` directive, we have changed the port from `:443` to `:80`.
```shell title="Caddyfile" {10}
{
servers :80 {
timeouts {
read_body 120s
}
}
}
# Replace the example with your domain name or IP address
:80 {
root * /var/www/pterodactyl/public
file_server
php_fastcgi unix//run/php/php8.3-fpm.sock {
root /var/www/pterodactyl/public
index index.php
env PHP_VALUE "upload_max_filesize = 100M
post_max_size = 100M"
env HTTP_PROXY ""
# env HTTPS "on" # IMPORTANT: this is commented out, to disable HTTPS
read_timeout 300s
dial_timeout 300s
write_timeout 300s
}
header X-Content-Type-Options "nosniff"
header X-XSS-Protection "1; mode=block;"
header X-Robots-Tag "none"
header Content-Security-Policy "frame-ancestors 'self'"
header X-Frame-Options "DENY"
header Referrer-Policy "same-origin"
request_body {
max_size 100m
}
respond /.ht* 403
log {
output file /var/log/caddy/pterodactyl.log {
roll_size 100MiB
roll_keep_for 7d
}
level INFO
}
}
```
### Enabling Configuration [#enabling-configuration-5]
The final step is to restart Caddy.
```bash
systemctl restart caddy
```
# About
## Core Project Team [#core-project-team]
| Name | GitHub | Primary Role |
| ------------- | ---------------------------------------------------------- | ------------------ |
| Robert Dennis | [@robertdrakedennis](https://github.com/robertdrakedennis) | Project Maintainer |
| Sky Mulley | [@SkyMulley](https://github.com/SkyMulley) | Project Maintainer |
Members of the project team have a red username in our Discord server.
## Community Team [#community-team]
Pterodactyl owes much of its success to our community support team. Its members have a yellow username in our Discord server.
## Sponsors [#sponsors]
These companies help fund Pterodactyl's development. [Interested in becoming a sponsor?](https://github.com/sponsors/pterodactyl)
| Company | About |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [**Buildurly**](https://buildurly.com/) | Buildurly is a hardware procurement company. They deliver tailored, enterprise-grade hardware solutions designed around your unique needs. From sourcing to delivery, Buildurly's white-glove service ensures a seamless, worry-free, professional experience. |
| [**BuiltByBit**](https://builtbybit.com/) | BuiltByBit is a marketplace for game server and community assets. Creators sell plugins, setups, builds, bots and websites for Minecraft, Roblox, Hytale, Garry's Mod and Discord. |
| [**Hosturly**](https://hosturly.com/) | Hosturly is an enterprise hosting provider. They provide cost-effective, high-performance, and reliable services, including VPS, Web, Dedicated, and Colocation. |
| [**indifferent broccoli**](https://indifferentbroccoli.com/) | indifferent broccoli is a game server hosting and rental company. With them, you get top-notch computer power for your gaming sessions. They destroy lag, latency, and complexity--letting you focus on the fun stuff. |
| [**Infraly, LLC**](https://infraly.co/) | Infraly is an infrastructure company powering the next generation of online services. Through their brands, Infraly delivers cutting-edge solutions across multiple markets. Their vertically integrated approach provides unmatched performance, scalability, and reliability, giving our customers full control. |
| [**MineStrator**](https://minestrator.com/) | MineStrator is a game server hosting provider. Looking for the most high-end French hosting company for your Minecraft server? More than 24,000 members on our Discord trust us. Give us a try! |
| [**Physgun**](https://physgun.com/) | Physgun is a game server hosting provider. Most providers rent rack space and rebrand a panel. At Physgun, they engineer the performance, write the features, and staff the support. Physgun truly is game hosting perfected! |
| [**WISP**](https://wisp.gg/) | WISP is an industry-leading SaaS platform for game server management, designed for hosting companies, gaming organizations, and enthusiasts. WISP combines modern, intuitive interfaces with powerful tools, making server deployment and administration seamless, scalable, and efficient. |
## License [#license]
Copyright © 2015 Pterodactyl®.
Code released under the [MIT License](https://github.com/pterodactyl/panel/blob/develop/LICENSE.md).
## Release Signing [#release-signing]
Previously, releases were signed by a GPG key. All recent releases are now signed using an SSH signing key.
This key is used to sign release tags and commits created by Matthew Penner. This key was first used to sign
`v1.10.2` for the Panel and `v1.7.1` for Wings and has been used ever since.
```text
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKL873MsP1OFfffNC8n9WcVuOXOSW65/q26MIzib0K9k
```
# Community Standards
Pterodactyl has grown from a community of tens in 2015 to a community of thousands in 2020. During that time
there have been countless growing pains and community has changed in an innumerable number of ways. At our heart
however, Pterodactyl continues to exist for one purpose: to be *the* platform for running your game servers.
In order to keep true to that goal, and continue to foster one of the largest open-source game panel communities
out there, we've adopted a simple set of guidelines for participating in this community. The goal of these guidelines
is to foster an inclusive, welcoming environment for new users, and provide a space for the thousands of existing
users, administrators, network owners, and hosting companies to co-exist.
These rules and guidelines extend to all facets of the Pterodactyl Community, including but not limited to our
Discord Server and all activities within the GitHub Organization.
## Community Guidelines [#community-guidelines]
At the most basic level, these guidelines can be distilled down to:
1. Be a decent human.
2. Patience is a virtue.
### Be Mature [#be-mature]
You are expected to be mature and control your behavior in a manner that adheres to basic human decency. If you are
unable to do this you will be removed from the community. Personal attacks, spam (in any form), "doxxing", or otherwise
acting out is not allowed.
This community is fairly lax in regards to moderating language. However, the following are some examples of
behavior that is absolutely *not* tolerated and for which you will be removed from the community.
* Racist, sexist, homophobic, transphobic, or otherwise derogatory speech, images, insinuations, or any language whose
sole purpose is to denigrate any individual, organization, or class of individual.
* Threats of violence against any person, group, or organization including "doxxing" of these entities.
* Pornographic or excessively violent content.
### Limit the Drama [#limit-the-drama]
Discussion, including linking to or discussing sites or software, that exists to cast a negative image of other
companies or users is not allowed. This includes calling out hosts using nulled software, attempting to elicit negative
reactions towards services or websites, or otherwise stirring up drama.
Assume someone is acting in good faith when responding to them. You don't have to agree with everyone, and you
don't need to respond to everything.
### Be Patient [#be-patient]
This is an open-source project. No members of the development team are paid in an official capacity to write,
maintain, nor support this software. The following actions are discouraged in this community.
* Repeatedly asking identical questions within the same channel (or across channels) within short periods of time.
* It is expected that some questions will be missed. If it has been a reasonable amount of time and your question
remains unanswered, you're welcome to re-post it.
* Keep all support questions within the realm of the support channels.
* Do not interrupt conversations in non-support channels solely to request that someone look in a support channel
and help you.
### No Commercial Services [#no-commercial-services]
Discussion of paid installation/upgrade services, modifications, or any other commercial offerings is strictly
prohibited unless otherwise noted. This also includes reaching out to individuals via Direct Message and offering
your services without provocation.
Advertising commercial services within your username or display name on Discord is forbidden.
[Sponsors](./about.mdx#sponsors) at the silver tier and higher are exempt from this rule.
### No Mention or Ping Spam [#no-mention-or-ping-spam]
Please, do not direct message any administrative, development, or notable community members without first
checking with them. Keep all support queries within the public support channels unless you have been directly
asked to move it elsewhere.
*But what if I am trying to respond back to someone?* That is fine! We only ask that you not mention people
directly if they're not already involved in a discussion with you.
# Introduction
Pterodactyl is the open-source game server management panel built with PHP, React, and Go. Designed with
security in mind, Pterodactyl runs all game servers in isolated Docker containers while exposing a beautiful
and intuitive UI to administrators and users. What more are you waiting for? Make game servers a first-class
citizen on your platform today.
## Supported Games [#supported-games]
We support a huge variety of games by utilizing Docker containers to isolate each instance, giving you the power
to host your games across the world without having to bloat each physical machine with additional dependencies.
Some of our core supported games include:
* Minecraft — including Spigot, Sponge, Bungeecord, Waterfall, and more
* Rust
* Terraria
* Teamspeak
* Mumble
* Team Fortress 2
* Counter-Strike: Global Offensive
* Garry's Mod
* ARK: Survival Evolved
In addition to our standard nest of supported games, our community is constantly pushing the limits of this software
and there are plenty more games available provided by the community. Some of these games include:
* Factorio
* San Andreas: MP
* Pocketmine MP
* Squad
* FiveM
* Xonotic
* Discord ATLBot
* [and many more...](https://pterodactyleggs.com)
## Responsible Disclosure [#responsible-disclosure]
Pterodactyl is completely open-source, and as such completely open to independent users and auditors to browse our
code base and hunt for security issues. If you come across anything that raises red flags for you, please do not
hesitate to reach out directly to `support@pterodactyl.io`. We ask that you please be responsible when disclosing
any security concerns and *do not* report them on our public facing bug tracker.
# Terminology
**Panel** — This refers to Pterodactyl Panel itself, and is what allows you to add additional
nodes and servers to the system.
**Node** — A node is a physical machine that runs an instance of Wings.
**Wings** — The newer service written in Go that interfaces with Docker and the Panel to provide secure access for
controlling servers via the Panel.
**Server** — In this case, a server refers to a running instance that is created by the panel. These servers are
created on nodes, and you can have multiple servers per node.
**Docker** — Docker is a platform that lets you separate the application from your infrastructure into isolated, secure containers.
**Docker Image** — A Docker image contains everything needed to run a containerized application. (e.g. Java for a Minecraft Server).
**Container** — Each server will be running inside an isolated container to enforce hardware limitations
(such as CPU and RAM) and avoid any interference between servers on one node. These are created by Docker.
**Nest** — Each nest is usually used as a specific game or service, for example: Minecraft, Teamspeak or Terraria and can contain many eggs.
**Egg** — Each egg is usually used to store the configuration of a specific type of game, for example: Vanilla, Spigot or Bungeecord for Minecraft.
**Yolks** — A curated collection of core docker images that can be used with Pterodactyl's Egg system.
## Simple Setup Diagram [#simple-setup-diagram]
## Advanced Setup Diagram [#advanced-setup-diagram]
It is also possible to install wings on the panel machine so it acts as panel and node machine at once.
# Additional Configuration
These are advanced configurations for Wings. You risk breaking Wings and making containers unusable if
you misconfigure something. Proceed only if you know what each configuration value does.
You must apply all changes to your Wings `config.yml` file located at `/etc/pterodactyl` and restart wings. Verify your config file using [Yaml Lint](http://www.yamllint.com/) should you receive errors related to YAML parsing.
## Private Registries [#private-registries]
You can use these settings to authenticate against (private) docker registries when pulling images.
### Available Keys [#available-keys]
| Setting Key | Default Value | Notes |
| ----------- | :-----------: | ----------------- |
| name | null | Registry address |
| username | null | Registry username |
| password | null | Registry password |
### Example of usage [#example-of-usage]
```yml
docker:
registries:
registry.example.com:
username: "registryusername"
password: "registrypassword"
```
## Custom Network Interfaces [#custom-network-interfaces]
You can change the network interface that Wings uses for all containers by editing the network name; it is by default set to `pterodactyl_nw`. For example, to enable Docker host mode change the network name to `host`.
Changing network mode to `host` grants Pterodactyl direct access to all machine interfaces and Panel users can bind to any IP or Port even if it's not allocated to their container. You will lose all benefits of Docker network isolation. It is not recommended for public installations that are hosting other users' servers.
### Example of usage [#example-of-usage-1]
```yml
docker:
network:
name: host
network_mode: host
```
After making changes, the following commands will stop the Wings, remove the Pterodactyl network, and start the Wings again. Run at your own risk.
`systemctl stop wings && docker network rm pterodactyl_nw && systemctl start wings`
## Enabling Cloudflare proxy [#enabling-cloudflare-proxy]
Cloudflare proxying of the Wings isn't beneficial since users will be connecting to the machine directly and bypassing any Cloudflare protection. As such, your Node machine IP will still be exposed.
To enable Cloudflare proxy, you must change the Wings port to one of the Cloudflare HTTPS ports with caching enabled (more info [here](https://developers.cloudflare.com/fundamentals/get-started/reference/network-ports/)), such as 8443, because Cloudflare only supports HTTP on port 8080. Select your Node in the Admin Panel, and on the settings tab, change the port. Make sure that you set "Not Behind Proxy" when using Full SSL settings in Cloudflare. Then on Cloudflare dashboard, your FQDN must have an orange cloud enabled beside it.
You are unable to proxy the SFTP port through Cloudflare unless you have their enterprise plan.
## Container PID Limit [#container-pid-limit]
You can change the total number of processes that can be active in a container at any given moment by changing the `container_pid_limit` value. The default value is `512`.
You can set it to `0` to disable the limit completely. However, this is *not* recommended as the limit prevents malicious overloading of the node.
Restart wings and your game server to apply the new limit.
### Example of usage [#example-of-usage-2]
```yml
docker:
...
container_pid_limit: 512
...
```
## Throttles Limits [#throttles-limits]
You can use these settings to adjust or completely disable throttling.
| Setting Key | Default Value | Notes |
| :---------------------- | :-----------: | ----------------------------------------------------------------------------------------------------------------------------------- |
| enabled | true | Whether or not the throttler is enabled |
| lines | 2000 | Total lines that can be output in a given line\_reset\_interval period |
| maximum\_trigger\_count | 5 | Amount of times throttle limit can be triggered before the server will be stopped |
| line\_reset\_interval | 100 | The amount of time after which the number of lines processed is reset to 0 |
| decay\_interval | 10000 | Time in milliseconds that must pass without triggering throttle limit before trigger count is decremented |
| stop\_grace\_period | 15 | Time that a server is allowed to be stopping for before it is terminated forcefully if it triggers output throttle |
| write\_limit | 0 | Impose I/O write limit for backups to the disk, 0 = unlimited. Value greater than 0 throttles write speed to the set value in MiB/s |
| download\_limit | 0 | Impose a Network I/O read limit for archives, 0 = unlimited. Value greater than 0 throttles read speed to the set value in MiB/s |
### Example of usage [#example-of-usage-3]
```yml
throttles:
enabled: true
lines: 2000
maximum_trigger_count: 5
line_reset_interval: 100
decay_interval: 10000
stop_grace_period: 15
```
## Installer Limits [#installer-limits]
Defines the limits on the installer containers that prevents a server's installation process from unintentionally consuming more resources than expected. This is used in conjunction with the server's defined limits. Whichever value is higher will take precedence in the install containers.
| Setting Key | Default Value | Notes |
| :---------- | :-----------: | ----------------------------------------------------------------------------------------------------------- |
| memory | 1024 | The maximum amount of memory install container can use unless server memory limit is higher than this value |
| cpu | 100 | The maximum amount of cpu install container can use unless server cpu limit is higher than this value |
### Example of usage [#example-of-usage-4]
```yml
installer_limits:
memory: 1024
cpu: 100
```
## Other values [#other-values]
More commonly discussed values. View all Wings config values and explanations in [these two files.](https://github.com/pterodactyl/wings/tree/develop/config)
| Setting Key | Default Value | Notes |
| ------------------------------ | :-----------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| debug | false | Force Wings to run in debug mode |
| tmpfs\_size | 100 | The size of the /tmp directory in MB when mounted into a container |
| websocket\_log\_count | 150 | The number of lines to display in the console |
| detect\_clean\_exit\_as\_crash | true | Mark server as crashed if it's stopped without user interaction, e.g., not pressing stop button |
| (crash detection) timeout | 60 | Timeout between server crashes that will not cause the server to be automatically restarted |
| app\_name | "Pterodactyl" | Changes the name of the daemon, shown in the panel's game console |
| check\_permissions\_on\_boot | true | Check all file permissions on each boot. Disable this when you have a very large amount of files and the server startup is hanging on checking permissions |
# Wings
Wings is the server control plane from Pterodactyl. It has been rebuilt from the ground up using Go and lessons learned from our first Nodejs Daemon.
## Getting Started [#getting-started]
If you're installing Wings for the first time, start with the [Installing Wings](/v1/wings/installing) guide.
If you're migrating from the old Node.JS daemon (0.6.x), see the [Migrating to Wings](/v1/wings/migrating) guide.
## Documentation Sections [#documentation-sections]
### Wings 1.0 (Current) [#wings-10-current]
* [Installing](/v1/wings/installing) - Complete installation guide for Wings
* [Configuration](/v1/wings/configuration) - Advanced configuration options
* [Upgrading](/v1/wings/upgrading) - Guide for upgrading Wings
# Installing Wings
Wings is the next generation server control plane from Pterodactyl. It has been rebuilt from the
ground up using Go and lessons learned from our first Nodejs Daemon.
You should only install Wings if you are running **Pterodactyl 1.x**. Do not install this software
for previous versions of Pterodactyl.
## Supported Systems [#supported-systems]
The following is a list of supported operating systems. Please be aware that this is not an exhaustive list,
there is a high probability that you can run the software on other Linux distributions without much effort.
You are responsible for determining which packages may be necessary on those systems. There is also a very
high probability that new releases of the supported OSes below will work just fine, you are not restricted to
only the versions listed below.
| Operating System | Version | Supported | Notes |
| ---------------------------------- | ------- | :-------: | --------------------------------------------------- |
| **Ubuntu** | 22.04 | ✅ | |
| | 24.04 | ✅ | |
| **RHEL / Rocky Linux / AlmaLinux** | 8 | ✅ | |
| | 9 | ✅ | |
| **Debian** | 11 | ✅ | |
| | 12 | ✅ | |
| | 13 | ✅ | |
| **Windows** | All | ❌ | This software will not run in Windows environments. |
## System Requirements [#system-requirements]
To run Wings, you will need a Linux system capable of running Docker containers. Most VPS and almost all
dedicated servers should be capable of running Docker, but there are edge cases.
When your provider uses `Virtuozzo`, `OpenVZ` (or `OVZ`), or `LXC` virtualization, you will most likely be unable to
run Wings. Some providers have made the necessary changes for nested virtualization to support Docker. Ask your provider's support team to make sure. KVM is guaranteed to work.
The easiest way to check is to type `systemd-detect-virt`.
If the result doesn't contain `OpenVZ` or`LXC`, it should be fine. The result of `none` will appear when running dedicated hardware without any virtualization.
Should that not work for some reason, or you're still unsure, you can also run the command below.
```bash
dane@pterodactyl:~$ sudo dmidecode -s system-manufacturer
VMware, Inc.
```
## Dependencies [#dependencies]
* curl
* Docker
### Installing Docker [#installing-docker]
For a quick install of Docker CE, you can execute the command below:
```bash
curl -sSL https://get.docker.com/ | CHANNEL=stable bash
```
If you would rather do a manual installation, please reference the [official Docker documentation](https://docs.docker.com/engine/install/) for how to install Docker CE on your server.
Please be aware that some hosts install a modified kernel that does not support important docker features. Please
check your kernel by running `uname -r`. If your kernel ends in `-xxxx-grs-ipv6-64` or `-xxxx-mod-std-ipv6-64` you're
probably using a non-supported kernel. Ask your host to switch your server to your distribution's standard kernel.
#### Start Docker on Boot [#start-docker-on-boot]
If you are on an operating system with systemd (Ubuntu 16+, Debian 8+, CentOS 7+) run the command below to have Docker start when you boot your machine.
```bash
sudo systemctl enable --now docker
```
#### Enabling Swap [#enabling-swap]
Since the version 6.1 of the Linux kernel, swap is enabled by default. If you are running a kernel version 6.1 or newer, you can skip this step. To check your kernel version, run `uname -r`.
On most systems, Docker will be unable to setup swap space by default. You can confirm this by running `docker info` and looking for the output of `WARNING: No swap limit support` near the bottom.
Enabling swap is entirely optional, but we recommended doing it if you will be hosting for others and to prevent OOM errors.
To enable swap, open `/etc/default/grub` as a root user and find the line starting with `GRUB_CMDLINE_LINUX_DEFAULT`. Make
sure the line includes `swapaccount=1` somewhere inside the double-quotes.
After that, run `sudo update-grub` followed by `sudo reboot` to restart the server and have swap enabled.
Below is an example of what the line should look like, *do not copy this line verbatim. It often has additional OS-specific parameters.*
```text
GRUB_CMDLINE_LINUX_DEFAULT="swapaccount=1"
```
Some Linux distros may ignore `GRUB_CMDLINE_LINUX_DEFAULT`. Therefore you might have to use `GRUB_CMDLINE_LINUX` instead should the default one not work for you.
## Installing Wings [#installing-wings]
The first step for installing Wings is to ensure we have the required directory structure setup. To do so,
run the commands below, which will create the base directory and download the wings executable.
```bash
sudo mkdir -p /etc/pterodactyl
curl -L -o /usr/local/bin/wings "https://github.com/pterodactyl/wings/releases/latest/download/wings_linux_$([[ "$(uname -m)" == "x86_64" ]] && echo "amd64" || echo "arm64")"
sudo chmod u+x /usr/local/bin/wings
```
If you are using a server provided by OVH or SoYouStart please be aware that your main drive space is probably allocated to
`/home`, and not `/` by default. Please consider using `/home/daemon-data` for server data. This can be easily
set when creating the node.
## Configure [#configure]
Once you have installed Wings and the required components, the next step is to create a node on your installed Panel. Go to your Panel administrative view, select Nodes from the sidebar, and on the right side click Create New button.
After you have created a node, click on it and there will be a tab called Configuration. Copy the code block content, paste it into a new file called `config.yml` in `/etc/pterodactyl` and save it.
Alternatively, you can click on the Generate Token button, copy the bash command and paste it into your terminal.
When your Panel is using SSL, the Wings must also have one created for its FQDN. See the documentation for creating SSL certificates before continuing.
### Starting Wings [#starting-wings]
To start Wings, simply run the command below, which will start it in debug mode. Once you confirmed that it is running without errors, use `CTRL+C` to terminate the process and daemonize it by following the instructions below. Depending on your server's internet connection pulling and starting Wings for the first time may take a few minutes.
```bash
sudo wings --debug
```
### Daemonizing (using systemd) [#daemonizing-using-systemd]
Running Wings in the background is a simple task, just make sure that it runs without errors before doing
this. Place the contents below in a file called `wings.service` in the `/etc/systemd/system` directory.
```text
[Unit]
Description=Pterodactyl Wings Daemon
After=docker.service
Requires=docker.service
PartOf=docker.service
[Service]
User=root
WorkingDirectory=/etc/pterodactyl
LimitNOFILE=4096
PIDFile=/var/run/wings/daemon.pid
ExecStart=/usr/local/bin/wings
Restart=on-failure
StartLimitInterval=180
StartLimitBurst=30
RestartSec=5s
[Install]
WantedBy=multi-user.target
```
Then, run the commands below to reload systemd and start Wings.
```bash
sudo systemctl enable --now wings
```
### Node Allocations [#node-allocations]
Allocation is a combination of IP and Port that you can assign to a server. Each created server must have at least one allocation. The allocation would be the IP address of your network interface. In some cases, such as when behind NAT, it would be the internal IP. To create new allocations go to Nodes > your node > Allocation.
Type `hostname -I | awk '{print $1}'` to find the IP to be used for the allocation. Alternatively, you can type `ip addr | grep "inet "` to see all your available interfaces and IP addresses. Do not use 127.0.0.1 for allocations.
# Migrating to Wings
This guide is for people looking to migrate from the old Node.JS daemon to Wings. Please see the
[install guide](/v1/wings/installing) if you are trying to install Wings for the first time on
a new node.
You **must** be running Pterodactyl Panel 1.X in order to use Wings.
You'll have a brief offline period as you perform this process, however no running game processes
will be affected. Plus, chances are your Panel will be offline (or in maintenance mode) during this
so your users should not notice anything out of the ordinary.
## Install Wings [#install-wings]
The first step for installing the daemon is to make sure we have the required directory structure setup. To do so,
run the commands below which will create the base directory and download the wings executable.
```bash
mkdir -p /etc/pterodactyl
curl -L -o /usr/local/bin/wings "https://github.com/pterodactyl/wings/releases/latest/download/wings_linux_$([[ "$(uname -m)" == "x86_64" ]] && echo "amd64" || echo "arm64")"
chmod u+x /usr/local/bin/wings
```
## Copy New Configuration File [#copy-new-configuration-file]
Once you have installed Wings, you'll need to copy over a new configuration file from the Panel. This file
is in a new format, and should be easier for you to manage and edit in the future.
Simply copy and paste the code block and paste it into a file called `config.yml` within the `/etc/pterodactyl`
directory and save it.
Please note that any modifications you previously made to the configuration will be lost with this. If you have
modifications to our default settings, the best option is to start Wings once with the copied configuration which
will then populate all of the other configuration settings.
From there you can make any adjustments as necessary.
## Remove Old Daemon [#remove-old-daemon]
Now that Wings is installed, we need to remove all of the old daemon code from the server since it is not being
used anymore. To do this, simply execute the following commands — assuming your old daemon is in the default
`/srv/daemon` directory.
```bash
# Stop the old daemon.
systemctl stop wings
# Delete the entire directory. There is nothing stored in here that we actually need for the
# purposes of this migration. Remember, server data is stored in /srv/daemon-data.
rm -rf /srv/daemon
# Optionally, remove NodeJS from your system if it was not used for anything else.
apt -y remove nodejs # or: yum remove nodejs
```
### Remove Standalone SFTP [#remove-standalone-sftp]
If you've used the standalone SFTP server with the old daemon, we need to remove it's systemd service as well, as it's no longer needed.
You can do so using the following commands.
```bash
# stop and disable the standalone sftp
systemctl disable --now pterosftp
# delete the systemd service
rm /etc/systemd/system/pterosftp.service
```
## Daemonize Wings [#daemonize-wings]
You'll then need to edit your existing `systemd` service file for Wings to point to the new control software. To do
this, open `/etc/systemd/system/wings.service` and replace the entire contents of the file with the following:
```text
[Unit]
Description=Pterodactyl Wings Daemon
After=docker.service
[Service]
User=root
WorkingDirectory=/etc/pterodactyl
LimitNOFILE=4096
PIDFile=/var/run/wings/daemon.pid
ExecStart=/usr/local/bin/wings
Restart=on-failure
StartLimitInterval=600
[Install]
WantedBy=multi-user.target
```
Then, start wings.
```bash
systemctl daemon-reload
systemctl enable --now wings
```
If you encounter issues starting Wings at this point, run the following command to start Wings directly and check
for any specific error output.
```bash
sudo wings --debug
```
# Upgrading Wings
Upgrading Wings is a painless process and should take less than a minute to complete.
## Wings Version Requirements [#wings-version-requirements]
Each version of Pterodactyl Panel also has a corresponding minimum version of Wings that
is required for it to run. Please see the chart below for how these versions line up. In
most cases your base Wings version should match that of your Panel.
| Panel Version | Wings Version | Supported |
| ------------- | ------------- | --------- |
| 1.0.x | 1.0.x | |
| 1.1.x | 1.1.x | |
| 1.2.x | 1.2.x | |
| 1.3.x | 1.3.x | |
| 1.4.x | 1.4.x | |
| 1.5.x | 1.4.x | |
| 1.6.x | 1.4.x | |
| 1.7.x | 1.5.x | |
| 1.8.x | 1.6.x | |
| 1.9.x | 1.6.x | |
| 1.10.x | 1.7.x | |
| **1.11.x** | **1.11.x** | ✅ |
*NOTE: There are no 1.8.x, 1.9.x, or 1.10.x releases of Wings.*
## Download Updated Binary [#download-updated-binary]
First, download the updated wings binary into `/usr/local/bin`. You will need to stop Wings briefly. *Your running
servers **will not** be affected.*
```bash
systemctl stop wings
curl -L -o /usr/local/bin/wings "https://github.com/pterodactyl/wings/releases/latest/download/wings_linux_$([[ "$(uname -m)" == "x86_64" ]] && echo "amd64" || echo "arm64")"
chmod u+x /usr/local/bin/wings
```
## Restart Process [#restart-process]
Finally, restart the wings process. Your running servers will not be affected and any open
connections to the instance will re-connect automatically.
```bash
systemctl restart wings
```
# API Overview
## Introduction [#introduction]
The Panel has three APIs. Choose the one that matches what you want to do:
| API | Path | Use it to |
| ----------- | ------------------ | ----------------------------------------------------------------------- |
| Client | `/api/client` | Manage your account and the servers you have access to. |
| Application | `/api/application` | Manage users, nodes, and servers, for example from a billing system. |
| Admin | `/api/admin` | Manage the whole Panel, including eggs, tags, settings, and extensions. |
The Panel's own login pages use the browser authentication endpoints under `/auth`.
The paths above are the same in every version. The `v2` in this documentation's address is not part of the API path.
In the examples, replace `https://panel.example.com` with the address of your Panel.
## Authentication [#authentication]
Send your API key in the `Authorization` header of every request:
```http
Authorization: Bearer YOUR_API_KEY
```
Each API accepts different keys:
| API | Accepted keys |
| ----------- | ----------------------------------------------------------------- |
| Client | A Client API key. It can only do what its user can do. |
| Application | An Application API key, or a root administrator's Client API key. |
| Admin | A root administrator's Client API key. |
Application API keys only work with the Application API, where each key is limited to the permissions it was created with. The Client API and the Admin API reject them.
API keys created on 1.x keep working after you upgrade.
### Browser Sessions [#browser-sessions]
The Panel's frontend signs in with a session cookie instead of an API key. First request `/sanctum/csrf-cookie`, then keep the cookies it returns, and send the CSRF token with every request that changes data.
The examples use a session cookie named `pterodactyl_session`. Your Panel may use a different name if you have set `APP_NAME` or `SESSION_COOKIE`.
## Where to Start [#where-to-start]
* [List your servers](./client-api/clientListClientServers.mdx) with the Client API.
* [List users](./application-api/applicationListUsers.mdx) with the Application API.
* [List users](./admin-api/adminListUsers.mdx) with the Admin API.
* [Sign in](./authentication/authLogin.mdx) with a browser session.
If you are moving an integration from 1.x, read [Changes From 1.x](../upgrading/changes-from-v1.mdx) first. The nest endpoints have been removed.
# Backend
An extension's backend runs inside the Panel like a Laravel package. This page continues the Server Notes example from [Building Extensions](./building.mdx).
## The Service Provider [#the-service-provider]
The backend starts in a service provider. It works like a Laravel service provider and must extend `Pterodactyl\Extensions\ExtensionProvider`:
```php
registerApiRoutes();
$this->loadExtensionMigrations();
}
}
```
Use `register` for container bindings and `boot` for everything else. The base class provides helpers for routes, migrations, settings, views, and translations.
The Panel loads your provider on every request. If it throws an exception, the Panel skips your extension, shows the error in the admin area, and keeps running.
## Routes [#routes]
Put your routes in the `routes` directory. `registerApiRoutes` loads each of these files that exists, with its own URL prefix and middleware:
| File | URL prefix | Who can call it |
| ------------------------ | ---------------------------------------------- | ---------------------------------------------------- |
| `routes/client.php` | `/api/client/extensions/{id}` | Any signed-in user, or a Client API key. |
| `routes/server.php` | `/api/client/servers/{server}/extensions/{id}` | Users with access to the server. |
| `routes/admin.php` | `/api/admin/extensions/{id}` | Root administrators. |
| `routes/application.php` | `/api/application/extensions/{id}` | Root administrators, including Application API keys. |
These routes use the same authentication, two-factor, and rate-limit middleware as the Panel's own API, so your extension does not handle sign-in.
Server Notes adds two server routes:
```php
where('server_id', $server->id)->first();
return new JsonResponse([
'body' => $note->body ?? '',
'updated_at' => $note?->updated_at?->toAtomString(),
]);
}
```
For a web route instead of an API route, call `registerWebRoutes` or `registerAuthenticatedWebRoutes` with the path to a route file. Web routes are served under `/extensions/{id}`. See the [Extension Reference](./reference.mdx#route-helpers).
## Authorization [#authorization]
The Panel checks that the user may access the server before your server routes run. Users without access receive a `404` response.
To check a specific permission, call `can` with one of the Panel's [subuser permissions](./reference.mdx#permissions). The server owner and root administrators pass every check. Server Notes lets any user read the notes, but only users who may edit files can change them:
```php
public function update(Request $request, Server $server): JsonResponse
{
abort_unless($request->user()->can('file.update', $server), 403, 'You do not have permission to edit notes.');
// ...
}
```
To define permissions for your own features, call `registerPermissions` in `boot`:
```php
$this->registerPermissions('Manage server notes.', [
'read' => 'Read server notes.',
'update' => 'Change server notes.',
]);
```
These keys become `ext.server-notes.read` and `ext.server-notes.update` and appear in the server's subuser permission editor. Use them with `$user->can(...)`, `useServerPermission(...)`, and a server screen's `permission` list. A wildcard such as `ext.server-notes.*` covers only that extension's permissions.
## Database Migrations [#database-migrations]
Put your migrations in `database/migrations` and call `loadExtensionMigrations` in `boot`. The Panel runs them when an administrator enables your extension, before marking it enabled.
Start table names with `ext_` and your extension's ID, using underscores instead of hyphens, to keep them apart from the Panel's own tables:
```php
Schema::create('ext_server_notes_notes', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('server_id')->unique();
$table->text('body');
$table->timestamps();
$table->foreign('server_id')->references('id')->on('servers')->cascadeOnDelete();
});
```
Disabling or removing the extension keeps its tables, so reinstalling it brings its data back. Package recovery does not undo schema changes, so design migrations so you can identify and recover from a partially completed change.
Use Eloquent models for your tables as usual:
```php
registerSettings(new ExtensionSettingsDefinition($this->settings(), [
ExtensionSettingDefinition::make('max_length', 'max_length', 2000, ['required', 'integer', 'min:100', 'max:20000'])
->label('Maximum note length')
->help('The longest note, in characters, that users can save.')
->field('number')
->frontend()
->frontendType('number'),
]));
}
```
The `make` method takes the setting's storage key, form field name, default value, and validation rules. Give each setting rules that match the values you accept.
The `field` method chooses the form control: `text`, `password`, `number`, `toggle`, or `select`. For `select`, pass the options as the second argument:
```php
ExtensionSettingDefinition::make('mode', 'mode', 'simple', ['required', 'in:simple,advanced'])
->field('select', [
['value' => 'simple', 'label' => 'Simple'],
['value' => 'advanced', 'label' => 'Advanced'],
]);
```
To read a setting in backend code, type-hint `ExtensionSettingsRegistry $registry` in your controller or job and get your definition from it. Unsaved settings return their default value:
```php
use Pterodactyl\Services\Extensions\ExtensionSettingsRegistry;
$maxLength = $registry->get('server-notes')->get('max_length');
```
Server Notes uses the setting to validate new notes:
```php
public function update(Request $request, Server $server, ExtensionSettingsRegistry $settings): JsonResponse
{
abort_unless($request->user()->can('file.update', $server), 403, 'You do not have permission to edit notes.');
$data = $request->validate([
'body' => ['present', 'nullable', 'string', 'max:'.$settings->get('server-notes')->get('max_length')],
]);
$note = Note::query()->updateOrCreate(['server_id' => $server->id], ['body' => $data['body'] ?? '']);
return new JsonResponse([
'body' => $note->body,
'updated_at' => $note->updated_at->toAtomString(),
]);
}
```
The `frontend` method sends the setting's value to your frontend bundle as `config`. **Every signed-in user can read frontend settings**, so never mark a secret as a frontend setting. Pages shown before sign-in, such as the login page, still load your bundle, but with an empty `config`. `frontendType` declares the public value's type, such as `number`, `boolean`, or `string`, and the Panel checks it before exposing the value.
For values your extension manages itself and administrators should not edit, use the key-value store returned by `$this->settings()`. It has `get`, `set`, `forget`, and `all` methods.
## Scoped Settings and Secrets [#scoped-settings-and-secrets]
The store returned by `settings()` is global to your extension. Use `forUser($user)` or `forServer($server)` to keep separate values per persisted user or server:
```php
$preferences = $this->settings()->forUser($user);
$preferences->set('show_preview', true);
$serverSettings = $this->settings()->forServer($server);
$serverSettings->set('summary_enabled', true);
$serverSettings->setSecret('webhook_token', $token);
```
Read a secret with `get`. Use `setSecret` every time you replace it so it stays encrypted. The settings definition also supports `forUser` and `forServer` when scoped values need defaults, normalization, and validation.
For a secret in the administrator's settings form, add `secret()` to its definition:
```php
ExtensionSettingDefinition::make('webhook_token', 'webhook_token', '', ['nullable', 'string', 'max:2000'])
->label('Webhook token')
->secret();
```
A secret uses a password control, is encrypted in the database, and is returned blank to the admin form. An empty update keeps the saved secret. Secrets cannot use `frontend()`. `field('password')` on its own does not enable encryption.
## Server Operation Events [#server-operation-events]
To react to a completed server operation, register a listener with `listenToServerOperations` in your provider. It receives a `Pterodactyl\Events\Server\OperationCompleted` object with the server UUID, operation, success flag, and resource UUID.
The operations are `provision`, `install`, `reinstall`, and `backup`. Events are dispatched after the database transaction commits; provisioning is reported after Wings accepts the server. The values are read-only, so a listener does not depend on a model being available later.
See [Server Operation Events](./reference.mdx#server-operation-events) for the event fields. If a listener throws, the Panel records the failure against the extension.
## Job Progress [#job-progress]
For long-running work, inject `Pterodactyl\Services\Extensions\ExtensionJobProgress` into your controller or job. Create a snapshot for the user who starts the work:
```php
$job = $progress->begin('server-notes', $user, $server, 'ext.server-notes.update');
```
Return `$job->id` to the frontend and pass it to your worker, which updates it as the work proceeds:
```php
$progress->update('server-notes', $jobId, 40, 'Reading server notes.');
$progress->update('server-notes', $jobId, 100, 'Summary ready.', 'completed');
```
Progress only moves forward, from 0 to 100. The status is `running`, `completed`, or `failed`; a finished snapshot cannot be changed. Omit the server and permission arguments for a user-only job. User jobs, and server jobs without a permission, are visible only to their creator and root administrators. A server job with a permission is visible to users who still have that permission and access to the server.
Web requests and workers must use the same cache store. Snapshots expire according to `extensions.progress_retention_seconds`, which defaults to one hour after the last update. Store results or audit history separately.
To show the snapshot in a screen or slot, use `useExtensionJobProgress`. Pass the server UUID for a server job:
```tsx
import { Spinner, useExtensionJobProgress } from '@pterodactyl/sdk';
export function JobProgress({ id, serverUuid }: { id: string; serverUuid: string }) {
const progress = useExtensionJobProgress(id, { serverUuid });
if (progress.error) return
Progress is unavailable.
;
if (!progress.data) return ;
return {progress.data.message} ({progress.data.percent}%)
;
}
```
The hook polls while the job runs, reads the latest snapshot after reconnecting, and stops on completion, failure, or loss of access.
## Views and Translations [#views-and-translations]
For Blade views, put them in `resources/views` and call `loadExtensionViews`. For translations, put them in `resources/lang` and call `loadExtensionTranslations`. Both use the `ext-{id}` namespace, such as `ext-server-notes::notes.title`.
# Building Extensions
## Introduction [#introduction]
An extension has a PHP backend that runs inside the Panel like a Laravel package, a React frontend that the Panel loads in the browser, or both. This guide builds an example extension, **Server Notes**, that adds a **Notes** page to every server, shows the notes above the console, stores them in its own database table, and has a setting for the maximum note length.
Read [Extensions](./index.mdx) first to learn how extensions are installed and managed.
The guide has five parts:
1. **Building Extensions** (this page): create the extension and write its manifest.
2. [Backend](./backend.mdx): the service provider, routes, database, settings, and background work.
3. [Frontend](./frontend.mdx): slots, screens, tabs, table columns, and component replacements.
4. [Data and Styling](./data-and-styling.mdx): Panel data, your API, components, and translations.
5. [Testing and Packaging](./testing-and-packaging.mdx): tests, builds, packages, and security.
## Requirements [#requirements]
You need:
* A Pterodactyl 2.0 source checkout, set up as a development Panel.
* PHP 8.3 or newer and Composer.
* Node.js 22.12 or newer.
Extensions created from a Panel checkout use its local `@pterodactyl/sdk` package. After installing the Panel's frontend dependencies, build the SDK test runtime from the Panel root:
```bash
npm run sdk:testing
```
Extension tests use this runtime. Rebuild it when you change the SDK source.
## Creating an Extension [#creating-an-extension]
From your Panel checkout, run `p:extension:make`:
```bash
php artisan p:extension:make server-notes --name="Server Notes" --description="Keep shared notes on each server."
```
This creates the extension in the Panel's `extensions` directory. Install its frontend dependencies and build it:
```bash
cd extensions/server-notes
npm install
npm run typecheck
npm test
npm run build
cd ../..
```
Then install and enable it in your development Panel:
```bash
php artisan p:extension:install extensions/server-notes --enable
```
`p:extension:make` accepts these options:
| Option | Description |
| --------------- | ------------------------------------------------------------------------------------------------------------ |
| `--name` | Display name. Defaults to the ID in title case. |
| `--description` | Short description. |
| `--author` | Author's name. |
| `--prefix` | Tailwind prefix for the extension's classes, 2 to 12 lowercase letters. Defaults to one derived from the ID. |
| `--no-ui` | Create a backend-only extension, with no frontend. |
| `--out` | Create the extension in a different directory. |
| `--force` | Replace an existing directory. |
### Extension IDs [#extension-ids]
The ID names your extension everywhere: its directory, URLs, and settings. It must start with a lowercase letter, contain only lowercase letters, numbers, and hyphens, and be at most 48 characters. `pterodactyl`, `panel`, and `core` are reserved.
The extension's directory must have the same name as its ID.
### Directory Structure [#directory-structure]
With the files from this guide added, Server Notes looks like this:
```text
server-notes/
├── .github/workflows/ci.yml
├── .gitignore
├── extension.json
├── README.md
├── database/
│ └── migrations/
│ └── 2026_09_29_000000_create_ext_server_notes_notes_table.php
├── routes/
│ └── server.php
├── scripts/
│ └── externalize-api.mjs
├── src/
│ ├── ServerNotesProvider.php
│ ├── Http/Controllers/NoteController.php
│ ├── Models/Note.php
│ └── client/
│ ├── index.tsx
│ ├── styles.css
│ ├── api.ts
│ ├── config.ts
│ ├── NotePreview.tsx
│ └── screens/
│ ├── NotesScreen.tsx
│ └── NotesScreen.spec.tsx
├── openapi-ts.config.ts
├── package.json
├── postcss.config.mjs
├── tsconfig.json
├── vite.config.mjs
└── vitest.config.mjs
```
The command also creates sample client routes in `routes/client.php`, a `StatusController`, and a `ServerScreen` with a test. Server Notes replaces them with the files above. `openapi-ts.config.ts` and `scripts/externalize-api.mjs` generate API clients, and the GitHub Actions workflow runs type checks, tests, and builds. With `--no-ui`, the command creates only the manifest, provider, sample controller and routes, migrations directory, `.gitignore`, and `README.md`.
## The Manifest [#the-manifest]
The `extension.json` file in the extension's root tells the Panel what the extension is and how to load it:
```json
{
"$schema": "./node_modules/@pterodactyl/sdk/manifest.schema.json",
"id": "server-notes",
"name": "Server Notes",
"version": "1.0.0",
"requires": {
"panel": "^2.0.0-dev",
"sdk": "^2.0.0-beta.4",
"php": "^8.3"
},
"description": "Keep shared notes on each server.",
"provider": "ServerNotes\\ServerNotesProvider",
"autoload": {
"ServerNotes\\": "src"
},
"ui": {
"entry": "dist/client.js",
"mode": "native",
"prefix": "sn",
"screens": [
{
"id": "notes",
"area": "server",
"path": "notes",
"nav": { "label": "Notes" }
}
]
}
}
```
The `provider` and `autoload` keys load your PHP code. The `ui` key loads your frontend bundle and declares its pages. `ui.prefix` is the Tailwind prefix for your extension's classes, derived from the ID unless you pass `--prefix`. The Panel rejects a build whose Tailwind utilities or theme variables do not use it. Leave out the keys for any half you do not use. See the [Extension Reference](./reference.mdx#manifest) for every key.
### Icon [#icon]
Administrators see each extension as a card on the **Extensions** page. Without an `icon`, the card shows the extension's initials. Set `icon` to a [Lucide](https://lucide.dev/icons) icon name:
```json
"icon": "notebook-pen"
```
Or ship an image in the extension and give its path:
```json
"icon": "icon.png"
```
The image must be a PNG, JPEG, or WebP file of at most 512 KB and 2048 pixels on each side. Use a square image of at least 128 pixels. `p:extension:pack` includes it in the package wherever it is, and `p:extension:doctor` warns if the Panel cannot show it. See [Extension Icons](./reference.mdx#extension-icons) for the full rules.
### Compatibility [#compatibility]
Declare the versions your extension supports with `requires.panel`, `requires.sdk`, and `requires.php`. Constraints use Composer syntax, such as `^2.0.0-beta.4` or `>=8.3 <9.0`. The values above match the development Panel and its SDK.
To depend on another extension, declare its ID and version constraint under `requires.extensions`:
```json
"requires": {
"extensions": {
"shared-notes": "^1.0"
}
}
```
The required extension must be enabled before yours can be. The Panel loads dependencies first, rejects dependency cycles, and skips extensions with unmet requirements. If a dependency fails, its dependents do not load either.
The `$schema` entry enables manifest completion in editors that support JSON Schema. The Panel validates the manifest when the package is installed and enabled.
# Data and Styling
These tools work in any frontend component: slots, screens, columns, and replacements.
## Using Panel Data [#using-panel-data]
The SDK has hooks for the data your pages need. Server Notes uses two:
```tsx
import { useCurrentServerRequired, useServerPermission } from '@pterodactyl/sdk';
const server = useCurrentServerRequired();
const canEdit = useServerPermission('file.update');
```
`useCurrentServerRequired` returns the current server on server pages. `useServerPermission` returns whether the current user has a permission on that server. Other hooks return the current user, the site settings, and live server events. See the [SDK reference](./reference.mdx#hooks).
The SDK also exposes the Panel's queries and mutations for files, startup configuration, and backups:
```tsx
import { useCurrentServerRequired, useServerFiles } from '@pterodactyl/sdk';
const server = useCurrentServerRequired();
const files = useServerFiles(server.attributes.uuid, '/', (response) => response.data);
```
Pass the server UUID to these hooks so they share the native page's cache entries. Query option factories are available for `useQuery`, prefetching, or direct cache access. File writes, startup changes, and backup creation have mutation hooks that update or invalidate the same cache. Startup writes are serialized per server.
If your own endpoint changes native data, await `invalidateServerData(uuid, domains)` after it succeeds. Supported domains are `server`, `files`, `fileContent`, `startup`, and `backups`. See [Server Data](./reference.mdx#server-data) for queries and [Mutations](./reference.mdx#mutations) for writes.
## Calling Your API [#calling-your-api]
Call your backend with the SDK's `http` client. It is the client the Panel uses, so requests are already signed in. Server Notes keeps its API calls in `src/client/api.ts`:
```ts
import { http } from '@pterodactyl/sdk';
export interface Note {
body: string;
updated_at: string | null;
}
const noteUrl = (server: string) => `/api/client/servers/${server}/extensions/server-notes/note`;
export const noteQueryKey = (server: string) => ['server-notes', server] as const;
export async function getNote(server: string): Promise {
const { data } = await http.get(noteUrl(server));
return data;
}
export async function saveNote(server: string, body: string): Promise {
const { data } = await http.put(noteUrl(server), { body });
return data;
}
```
The Panel shares its copy of TanStack Query with extensions, so you can use `useQuery` and `useMutation` directly. The Notes screen loads and saves the note like this:
```tsx
import { useEffect, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
Button,
ServerContentBlock,
Spinner,
TextArea,
TitledGreyBox,
httpErrorToHuman,
toast,
useCurrentServerRequired,
useServerPermission,
} from '@pterodactyl/sdk';
import { getNote, noteQueryKey, saveNote } from '../api';
import { maxLength } from '../config';
export default function NotesScreen() {
const server = useCurrentServerRequired();
const identifier = server.attributes.identifier;
const canEdit = useServerPermission('file.update');
const queryClient = useQueryClient();
const [body, setBody] = useState('');
const note = useQuery({ queryKey: noteQueryKey(identifier), queryFn: () => getNote(identifier) });
useEffect(() => {
if (note.data) {
setBody(note.data.body);
}
}, [note.data]);
const save = useMutation({
mutationFn: () => saveNote(identifier, body),
onSuccess: (saved) => {
queryClient.setQueryData(noteQueryKey(identifier), saved);
toast.success('Notes saved.');
},
onError: (error) => toast.error(httpErrorToHuman(error)),
});
return (
{note.isLoading ? (
) : (
)}
);
}
```
`httpErrorToHuman` turns an API error into a message you can show the user.
## Generated API Clients [#generated-api-clients]
The scaffold includes an OpenAPI generator and a script that connects its generated client to the Panel's shared transport. Enable your extension so its routes are loaded, and document the controller parameters and responses for Scribe. Then generate the specification and extension client from the Panel root:
```bash
composer docs:openapi
npm run extension:api:generate -- server-notes
```
This extracts routes from the client, server, admin, and application API areas, writes `extensions/server-notes/openapi.yaml`, and generates code in `src/client/generated`. The generated functions and query options use `@pterodactyl/sdk/api`, so they keep the Panel's authentication and query client.
If you already have the extension's `openapi.yaml`, run `npm run api:generate` inside the extension package. This requires the scaffold's OpenAPI configuration and externalization script.
## Components and Styling [#components-and-styling]
The SDK includes the Panel's own components, such as `Button`, `Input`, `TextArea`, `Select`, `Switch`, `Dialog`, `Alert`, `TitledGreyBox`, and `ServerContentBlock`, so your pages match the Panel. `toast` shows notifications. See the [SDK reference](./reference.mdx#components) for the full list.
The scaffold's entry file imports `src/client/styles.css`, which includes Tailwind's theme and utilities plus the SDK's mapping to the Panel's design tokens:
```css title="src/client/styles.css"
@import 'tailwindcss/theme.css' layer(theme);
@import 'tailwindcss/utilities.css' layer(utilities);
@import '@pterodactyl/sdk/theme.css';
@source './**/*.{ts,tsx}';
```
Use classes such as `bg-card`, `text-foreground`, and `border-border` to follow the active theme. Keep Tailwind preflight out of extension styles so it does not reset the Panel's elements. Use an extension-specific prefix for any custom CSS selectors.
The Panel loads the entry stylesheet before calling `setup`, and loads styles for screen chunks when they are imported. The [theme guide](../guides/customization/themes.mdx#tokens) lists the shared tokens.
## Frontend Translations [#frontend-translations]
Call `loadExtensionTranslations()` in your provider and add a translation group, such as `resources/lang/en/messages.php`. Read it in screens and slots with `useExtensionTranslation('messages')`:
```tsx
import { useExtensionTranslation } from '@pterodactyl/sdk';
const { t, ready } = useExtensionTranslation('messages');
```
The hook uses the current Panel locale and the `ext-server-notes::messages` namespace. Use `ready` to handle loading and `t('title')` to read the group's `title` key.
## Shared Packages [#shared-packages]
The Panel provides React, React DOM, TanStack Query, and the SDK to your bundle at runtime. They are left out of your build, so every extension uses the Panel's copy. Any other package you import is bundled into your extension.
Import `toast` from the SDK, not from `sonner` directly, or your notifications will not appear.
## Configuration Types [#configuration-types]
Mark public settings with `frontend()` and their value type with `frontendType(...)`. Enable the extension so its definitions are loaded, then generate its TypeScript types from the Panel root:
```bash
php artisan p:extension:types extensions/server-notes
```
This writes `src/client/extension-types.ts` with the extension ID, literal screen IDs, registered permissions, and declared public configuration keys. Secret settings are excluded.
Use these types with `defineConfiguredExtension`, whose parser checks runtime values before your `setup` function uses them:
```tsx
import { defineConfiguredExtension } from '@pterodactyl/sdk';
import type { ExtensionConfig, ExtensionScreenId } from './extension-types';
import { setConfig } from './config';
import NotePreview from './NotePreview';
import './styles.css';
export default defineConfiguredExtension(
(config) => {
const maxLength = config.max_length ?? 2000;
if (typeof maxLength !== 'number') throw new Error('Invalid maximum note length.');
return { ...config, max_length: maxLength };
},
{
setup({ config, slots, screens }) {
setConfig(config);
screens.register('notes', () => import('./screens/NotesScreen'));
slots.register('server.console.before', NotePreview);
},
}
);
```
If your bundle also renders on pages before sign-in, handle the empty public configuration in your parser. Server Notes can fall back to the setting's default value there.
# Frontend
An extension's frontend is a React bundle that the Panel loads in the browser. This page continues the Server Notes example from [Building Extensions](./building.mdx).
## The Entry File [#the-entry-file]
The frontend starts in `src/client/index.tsx`, which default-exports an extension definition with a `setup` function:
```tsx
import { definePterodactylExtension } from '@pterodactyl/sdk';
import './styles.css';
import { setConfig } from './config';
import NotePreview from './NotePreview';
export default definePterodactylExtension({
setup({ config, slots, screens }) {
setConfig(config);
screens.register('notes', () => import('./screens/NotesScreen'));
slots.register('server.console.before', NotePreview);
},
});
```
The Panel calls `setup` once when the page loads. It receives:
* `meta`, with your extension's `id` and `version`.
* `config`, with the values of your [frontend settings](./backend.mdx#settings).
* `components`, to [replace supported Panel components](#component-replacements).
* `slots`, to place components on the Panel's pages.
* `screens`, to add your own pages.
* `columns`, to add columns to supported admin tables.
`setup` must register everything right away and cannot be `async`.
Screens do not receive `config` directly. To use it on a page, save it in a small module, as Server Notes does in `src/client/config.ts`:
```ts
import type { ExtensionConfig } from '@pterodactyl/sdk';
let config: ExtensionConfig = {};
export function setConfig(value: ExtensionConfig): void {
config = value;
}
export function maxLength(): number {
return typeof config.max_length === 'number' ? config.max_length : 2000;
}
```
## Slots [#slots]
Slots are named places on the Panel's pages where extensions add content, such as `server.console.before` above the server console. To use one, register a component for it:
```tsx
slots.register('server.console.before', NotePreview);
```
Each slot passes data for its location. Console slots receive the current server, file action slots receive file and selection context, and most other page slots receive route data. Server Notes uses the console slot's server to load the note:
```tsx
import { useQuery } from '@tanstack/react-query';
import { TitledGreyBox, type SdkServer } from '@pterodactyl/sdk';
import { getNote, noteQueryKey } from './api';
export default function NotePreview({ data: server }: { data: SdkServer }) {
const identifier = server.attributes.identifier;
const { data: note } = useQuery({
queryKey: noteQueryKey(identifier),
queryFn: () => getNote(identifier),
});
if (!note?.body) {
return null;
}
return (
{note.body.split('\n').slice(0, 3).join('\n')}
);
}
```
The [slot reference](./reference.mdx#slots) lists every slot and its `data`. If you register a slot name that does not exist, your extension does not load, and the error appears in the browser console.
When several extensions use the same slot, their components render in the Panel's configured extension order, with dependencies first. If your component throws an error, only it is hidden, and the user sees a notice with a retry option.
## Screens [#screens]
Screens are full pages. To add one, first declare it in `extension.json` under `ui.screens`:
```json
"screens": [
{
"id": "notes",
"area": "server",
"path": "notes",
"nav": { "label": "Notes" }
}
]
```
Then register its component in `setup` with the same ID. Load it with a dynamic `import` so its code only downloads when the page is opened:
```tsx
screens.register('notes', () => import('./screens/NotesScreen'));
```
The screen's `area` sets where the page lives:
| Area | URL | Who can open it |
| --------- | ------------------------- | -------------------------------- |
| `server` | `/server/{server}/{path}` | Users with access to the server. |
| `account` | `/account/{path}` | Any signed-in user. |
| `admin` | `/panel/{path}` | Root administrators. |
A screen with a `nav` label gets a link in its area's navigation, after the Panel's own items. Without `nav`, it has a URL but no navigation item. Use `nav.order` to sort extension items, and `nav.icon` or `nav.badge` to add an icon or short label.
On `server` screens, set `permission` to a list of subuser permissions to show the page and its navigation item only to users with at least one of them. Only server screens accept `permission`. Your backend routes must also authorize the request.
Choose a `path` the Panel does not already use in that area. Paths start with a static segment and may contain named parameters, such as `logs/$date`. Screen components receive `ScreenComponentProps`, whose `data.params.date` holds that value. `$id` is reserved for the Panel. A navigation item for a parameterized path must provide its values in `nav.params`.
Every screen declared in `extension.json` must be registered in `setup`. If one is missing or an ID does not match, the Panel does not load your extension.
## Detail Tabs and Navigation [#detail-tabs-and-navigation]
An admin screen can be a tab on a node, server, user, or egg detail page. Set `parent` to the matching resource and keep `area` as `admin`:
```json
{
"id": "node-notes",
"area": "admin",
"parent": "admin.node",
"path": "notes",
"nav": { "label": "Notes", "order": 10, "icon": "file" }
}
```
Register `node-notes` with `screens.register` as usual. Its URL is `/panel/nodes/{id}/notes`. Inside the screen, `useCurrentResource()` returns `{ kind: 'admin.node', resource }`; check `kind` before reading the resource's fields. The [screen reference](./reference.mdx#manifest) lists every parent and URL.
Use `PanelLink` for links and `usePanelNavigate` for actions; both use the Panel's router. You can name an extension screen instead of building its URL:
```tsx
import { PanelLink } from '@pterodactyl/sdk';
Open notes
```
For a core route, use `destination={{ to: '/server/$id/files', params: { id: identifier } }}`. `usePanelLocation()` returns the current path, search, and parameters.
## Table Columns [#table-columns]
To add a column to the native node, server, or egg table, call `columns.register` during `setup`:
```tsx
columns.register('admin.nodes', {
id: 'notes-host',
label: 'Notes host',
component: ({ data }) => {data.attributes.fqdn} ,
});
```
The `data` prop is typed for that table. Columns appear after the Panel's own columns, in extension order, and each cell has its own error boundary. See [Table Columns](./reference.mdx#table-columns) for the supported table names.
## Component Replacements [#component-replacements]
A component replacement changes a supported part of the Panel's presentation. The Panel still owns its data queries, navigation, permissions, selection, menus, and mutations.
Declare the component names in your manifest with an SDK constraint:
```json
"requires": {
"sdk": ">=2.0.0-beta.3 <3.0"
},
"ui": {
"entry": "dist/client.js",
"components": ["dashboard.serverCard", "server.files.details"]
}
```
In `setup`, register an implementation for every declared component, as a component or a lazy importer:
```tsx
export default definePterodactylExtension({
setup({ components }) {
components.replace('dashboard.serverCard', {
load: () => import('./ServerCard'),
});
components.replace('server.files.details', {
load: () => import('./FileDetails'),
});
},
});
```
In `src/client/FileDetails.tsx`, customize the name and keep the Panel's icon, size, and timestamp:
```tsx
import type { ComponentPartProps, ReplacementProps } from '@pterodactyl/sdk';
function FileName({ model }: ComponentPartProps<'server.files.details'>) {
return {model.name}
;
}
export default function FileDetails({ Default }: ReplacementProps<'server.files.details'>) {
return ;
}
```
In `src/client/ServerCard.tsx`, use the default card with your own spacing:
```tsx
import type { ReplacementProps } from '@pterodactyl/sdk';
export default function ServerCard({ Default }: ReplacementProps<'dashboard.serverCard'>) {
return ;
}
```
Each replacement receives a read-only `model`, the native `Default` component, and typed `parts`. `Default` accepts `className` and partial part replacements. For a complete layout, render your own markup and use native parts as needed, such as ` `. Define part components outside your render function so their identity stays stable. The native server-card identity part includes the `dashboard.serverRow.name.after` slot; include that part to keep that slot's contributions.
These presentation areas sit inside core links. Keep them free of buttons, links, inputs, and other interactive elements, and add actions through the existing action slots. See [Component Replacements](./reference.mdx#component-replacements) for models and parts.
Enabling an extension activates its replacements. Each component can have one enabled owner. The Panel rejects a conflicting enable or update and names the component and its owner. Disable the existing owner before enabling a competing extension.
All rows on a page share one loading decision. While an implementation loads, its area shows a skeleton. If setup or the implementation fails, or loading takes more than five seconds, the area uses the native view, and a late import cannot replace it on the same page. Render failures also restore the native presentation and keep core state. Keep asynchronous rendering inside a local `Suspense` boundary, and use `useExtensionAction` for extension callbacks. The Panel does not retry mutations when it restores a view.
Test your replacement against the native default and parts:
```tsx
import { render, screen } from '@testing-library/react';
import { expect, it } from 'vitest';
import { createComponentTestHost, createTestFileDetails } from '@pterodactyl/sdk/testing';
import FileDetails from './FileDetails';
it('shows the file name with native details', () => {
const host = createComponentTestHost('server.files.details', {
model: createTestFileDetails({ name: 'server.properties' }),
});
render( , { wrapper: host.Wrapper });
expect(screen.getByText('server.properties')).toBeTruthy();
expect(screen.getByText('1 KiB')).toBeTruthy();
});
```
`createTestServerCard` provides a server-card fixture; override its `state` to test loading, unavailable, or ready presentation. Component test hosts provide the native view and parts without issuing core queries.
## File Actions and Native Forms [#file-actions-and-native-forms]
The file manager has slots for its toolbar, row actions, and selected-file actions. They receive the current server, directory, files, selected names, and callbacks to refresh or change the selection. Row actions also receive their file.
This toolbar control refreshes the native file list:
```tsx
import { Button, useExtensionAction, type FileManagerSlotData } from '@pterodactyl/sdk';
function RefreshFiles({ data }: { data: FileManagerSlotData }) {
const refresh = useExtensionAction('refresh files', () => data.refresh());
return Refresh files ;
}
// In setup:
slots.register('server.files.toolbar', RefreshFiles);
```
Wrap asynchronous click handlers in `useExtensionAction` so failures are attributed to your extension. Use the SDK's file mutations for writes, and check the user's permission on both frontend and backend.
The `panel.users.detail.form` slot receives the native user form. Contributed controls and validators use its existing values and save with the Panel's **Save Changes** action:
```tsx
slots.register('panel.users.detail.form', ({ data }) => (
value.length < 3 ? 'Use at least three characters.' : undefined }}
>
{(field) => }
));
```
Use the existing fields in `AdminUserFormValues`. The slot does not add arbitrary fields to the Panel's user API, so keep extension-specific values in scoped settings or your own endpoint.
The `server.startup.form` slot receives the current startup configuration, pending state, and a `setDockerImage` callback that checks the user's permission and keeps the native cache current. See [Slot Data](./reference.mdx#slot-data) for these contracts.
# Extensions
## Introduction [#introduction]
Extensions add features to the Panel without changing its code. They can add pages, detail tabs, table columns, and API endpoints, place content on existing pages, and store their own data.
Each extension is a package described by an `extension.json` file and installed in the Panel's `extensions` directory.
Extensions run with the same access as the Panel and can read your database, settings, and users' data. Only install extensions from developers you trust.
To build your own extension, see [Building Extensions](./building.mdx).
## Installing Extensions [#installing-extensions]
Extensions are distributed as `.pteroext` files, which are zip archives of the extension's directory.
### From the Admin Area [#from-the-admin-area]
Open **Admin → Extensions**, click **Install**, and choose a `.pteroext` or `.zip` file of up to 50 MB. Select **Enable after install** to enable it right away.
### From the Command Line [#from-the-command-line]
Pass `p:extension:install` a `.pteroext` file, a `.zip` file, or an unpacked extension directory:
```bash
php artisan p:extension:install /path/to/server-notes.pteroext --enable
```
`--enable` enables it after install. Then give the web server user ownership of the new files:
```bash
chown -R www-data:www-data /var/www/pterodactyl/extensions /var/www/pterodactyl/public/assets/extensions
```
Installing an unpacked directory copies all of it, including development files such as `node_modules`. Install a packaged `.pteroext` file to avoid this.
## Enabling and Disabling Extensions [#enabling-and-disabling-extensions]
An installed extension does nothing until you enable it. Enable and disable extensions from **Admin → Extensions**, or with Artisan:
```bash
php artisan p:extension:enable server-notes
php artisan p:extension:disable server-notes
```
Before enabling an extension, the Panel checks its version requirements, required extensions, and built frontend files. It then runs the migrations in `database/migrations` and publishes the assets before marking the extension enabled. If activation fails, the extension keeps its previous state and asset version.
An extension can replace supported parts of the Panel's presentation. Enabling it activates those replacements; disabling it restores the native views after a reload. Each component can be replaced by only one enabled extension. A conflicting enable or update fails and names the component and its existing owner.
Required extensions must be enabled and meet the declared version constraints. Before disabling or removing an extension, disable the extensions that depend on it.
Disabling an extension turns off its pages, API endpoints, and frontend code, but keeps its files, data, and settings.
Installing, enabling, disabling, or removing an extension refreshes cached routes and signals queue workers to restart. Users must reload the Panel to get the current frontend code.
## Configuring Extensions [#configuring-extensions]
To change an extension's settings, open **Admin → Extensions** and click the extension's settings button. Settings only appear for enabled extensions.
Settings are stored in the Panel's database. Secret settings are encrypted and appear blank in the form; leave a secret field blank to keep its saved value. A password field is only encrypted if the extension declares it as a secret.
Changes to frontend settings take effect after a page reload.
## Updating Extensions [#updating-extensions]
To update an extension, install the new package the same way. The extension keeps its enabled or disabled state. If it is enabled, the Panel checks compatibility and runs the package's migrations before activating it.
If installation or activation fails, the Panel restores the previous package, version, enabled state, and asset version. It cannot undo database schema changes made by migrations. Keep a database backup, and check for partially applied migrations before trying again.
Published assets use versioned directories. The Panel keeps earlier builds so open browser sessions can still load their screen files during an update. Reload the page to use the installed version.
## Removing Extensions [#removing-extensions]
Remove an extension from **Admin → Extensions**, or with Artisan:
```bash
php artisan p:extension:remove server-notes
```
Removing an extension deletes its files and published assets. **Its database tables and settings are kept**, so its data is still there if you install it again. To remove the data, drop the extension's tables by hand. By convention, their names start with `ext_` followed by the extension's ID.
## Listing Extensions [#listing-extensions]
To list every installed extension and its state, run:
```bash
php artisan p:extension:list
```
```text
+--------------+--------------+---------+-----+---------+-------+
| ID | Name | Version | UI | State | Error |
+--------------+--------------+---------+-----+---------+-------+
| server-notes | Server Notes | 1.0.0 | yes | enabled | |
+--------------+--------------+---------+-----+---------+-------+
```
## Configuration [#configuration]
Set these environment variables in your `.env` file:
| Variable | Default | Description |
| ---------------------------------- | ----------------------------------- | ------------------------------------------------------------------------ |
| `PTERODACTYL_EXTENSIONS_ENABLED` | `true` | Set to `false` to stop loading all extensions without uninstalling them. |
| `PTERODACTYL_EXTENSIONS_DIRECTORY` | `extensions` in the Panel directory | Where extensions are installed. |
If an extension breaks your Panel, set `PTERODACTYL_EXTENSIONS_ENABLED=false` to turn off all extensions, then find and disable the broken one.
The web server user must be able to write to the extensions directory and to `public/assets/extensions`, or you cannot install or remove extensions from the admin area.
### Docker [#docker]
If you run the [Docker image](../panel/docker.mdx), keep `/app/extensions` and `/app/public/assets/extensions` on volumes, or your extensions are lost when the container is recreated. The example Compose file already does this.
## Troubleshooting [#troubleshooting]
### An Extension Shows an Error [#an-extension-shows-an-error]
If an extension fails while the Panel starts, the Panel skips it and keeps running. The error appears on the extension's card in **Admin → Extensions** and in the `Error` column of `p:extension:list`. The Panel retries the extension on every request, so the error clears once the problem is fixed.
An extension marked **invalid** has a broken `extension.json` file and cannot be enabled until the file is fixed.
### An Extension's Page or Content Does Not Appear [#an-extensions-page-or-content-does-not-appear]
When an extension's content fails to load, users see a recovery action: a retry for render failures, or a page reload for failed screen files. **Admin → Extensions** also shows frontend failures seen by the current browser. The browser's developer console has the full error.
Check that the extension is enabled, and reload the page. The Panel only loads extension changes on reload.
# Extension Reference
## Introduction [#introduction]
This page lists everything available to extensions. For a guided introduction, see [Building Extensions](./building.mdx).
## Manifest [#manifest]
`extension.json` supports these keys:
| Key | Required | Description |
| --------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id` | Yes | The extension's ID. Lowercase letters, numbers, and hyphens, starting with a letter, up to 48 characters. Must match the directory name. `pterodactyl`, `panel`, and `core` are reserved. |
| `name` | Yes | The display name. |
| `version` | Yes | The version, up to 64 characters. |
| `$schema` | No | Editor schema, usually `./node_modules/@pterodactyl/sdk/manifest.schema.json` in a frontend package. |
| `requires.panel` | No | Supported Panel versions, using Composer constraints. |
| `requires.sdk` | With component replacements | Supported SDK versions, using Composer constraints. |
| `requires.php` | No | Supported PHP versions, using Composer constraints. |
| `requires.extensions` | No | Required extension IDs mapped to version constraints. Each dependency must be enabled. |
| `description` | No | Short description shown in the admin area. |
| `author` | No | Author shown in the admin area. |
| `icon` | No | Icon shown for the extension on the admin **Extensions** page, up to 255 characters. Either a [Lucide](https://lucide.dev/icons) icon name in kebab-case, such as `life-buoy`, or the relative path of a PNG, JPEG, or WebP image inside the extension, such as `icon.png`. See [Extension Icons](#extension-icons). Without an icon, the page shows the extension's initials. |
| `provider` | No | Fully qualified class name of the service provider. Must extend `Pterodactyl\Extensions\ExtensionProvider`. |
| `autoload` | No | PSR-4 map of namespace prefixes to directories inside the extension, such as `{"ServerNotes\\": "src"}`. |
| `routes.root` | No | Top-level URL prefixes for `registerRootRoutes`, up to 8, such as `["go"]`. Each is a lowercase slug of up to 48 characters. Prefixes the Panel uses or another enabled extension claims are rejected. |
| `ui.entry` | With `ui` | The frontend bundle. Must be `dist/client.js`. |
| `ui.mode` | No | Must be `native` (the default). |
| `ui.prefix` | With Tailwind utilities | The Tailwind prefix for the extension's classes, 2 to 12 lowercase letters, such as `sn`. Tailwind variants, theme namespaces, and Panel names are reserved. Two enabled extensions cannot share a prefix. Builds whose Tailwind utilities or theme variables lack the prefix are rejected. |
| `ui.screens` | No | Pages the extension adds, up to 64. |
| `ui.components` | No | Supported component names this extension replaces. Names must be unique. |
Each entry in `ui.screens` supports these keys:
| Key | Required | Description |
| ------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | Yes | The screen's ID, used with `screens.register`. Lowercase letters, numbers, and hyphens, starting with a letter. |
| `area` | Yes | `server`, `account`, or `admin`. |
| `parent` | No | An admin detail page: `admin.node`, `admin.server`, `admin.user`, or `admin.egg`. Only accepted with `area: "admin"`. |
| `path` | Yes | A static first segment, followed by static or named parameter segments, such as `notes/$tab`. `$id` is reserved. Paths must not collide with core or extension routes. |
| `nav.label` | No | Navigation tab label, up to 100 characters. Without `nav`, the page has no tab. |
| `nav.exact` | No | Highlight the navigation item only on an exact path match. |
| `nav.params` | For parameterized navigation | Values for every named parameter in the screen path. |
| `nav.order` | No | Sort order among extension navigation items. Defaults to `0`. |
| `nav.group` | No | Group metadata shown as a tooltip, up to 100 characters. |
| `nav.badge` | No | Short label beside the navigation item, up to 32 characters. |
| `nav.icon` | No | Any [Lucide](https://lucide.dev/icons) icon name in kebab-case, up to 64 characters, such as `puzzle`, `life-buoy`, or `terminal`. A name this Panel does not ship renders the default icon, and `p:extension:doctor` warns about it. |
| `permission` | No | Only accepted on `server` screens. The page and navigation item require at least one of the listed subuser permissions. Backend routes must still authorize the request. |
| `when.eggTags` | No | Only accepted on `server` screens. `{ "any": [...], "all": [...] }`, matched without regard to case against the server's egg tags. `all` requires every value and `any` at least one. Each list holds 1 to 32 values. |
| `when.eggFeatures` | No | Like `when.eggTags`, matched against the server's egg features. |
| `when.match` | No | `all` (the default) or `any`. Combines `eggTags` and `eggFeatures`, and requires both. |
| `when.runtime` | No | `true` hides the screen until the `visible` predicate passed to `screens.register` returns `true`. Accepted in any area. |
A screen whose `when` conditions fail has no navigation item, and its URL shows the not-found page. `when` must declare an egg rule or `"runtime": true`.
Screen URLs use these roots, followed by the screen's `path`:
| Area or parent | URL root |
| -------------- | --------------------- |
| `account` | `/account` |
| `server` | `/server/{id}` |
| `admin` | `/panel` |
| `admin.node` | `/panel/nodes/{id}` |
| `admin.server` | `/panel/servers/{id}` |
| `admin.user` | `/panel/users/{id}` |
| `admin.egg` | `/panel/eggs/{eggId}` |
### Extension Icons [#extension-icons]
The `icon` key accepts two forms:
```json
"icon": "life-buoy"
```
```json
"icon": "resources/icon.png"
```
A value made of lowercase letters, numbers, and single hyphens is a Lucide icon name. A name this Panel does not ship renders the default icon, and `p:extension:doctor` warns about it.
A value ending in `.png`, `.jpg`, `.jpeg`, or `.webp` is an image path, relative to the extension's root and using `/` between directories. No directory or file name may start with a dot, so the path cannot leave the extension. The Panel accepts the image when:
* It is a PNG, JPEG, or WebP image, judged from the file's contents rather than its name. SVG is not supported.
* It is at most 512 KB and at most 2048 pixels on each side.
* It resolves to a file inside the extension. A symlink that points outside the extension is refused.
Use a square image of at least 128 pixels. The Panel shows it at 40 pixels, scaled to fit, on the extension's own background.
The Panel serves the image from the installed extension to administrators who can view extensions, at `/api/admin/extensions/{id}/icon`, so it shows while the extension is disabled. It does not need to be in `dist`. The `icon_url` field of the [Admin API](../api/index.mdx)'s extension list holds the image's URL, including a version that changes with the file's contents. An image that is missing or fails these checks leaves `icon_url` empty, and the page shows the extension's initials. The manifest stays valid, and `p:extension:doctor` warns about the image.
Compatibility is checked before activation. Dependencies load first; incompatible packages and dependency cycles are skipped. Disable dependents before disabling or removing a required extension.
## Artisan Commands [#artisan-commands]
| Command | Description |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `p:extension:make {id}` | Create a new extension. Options: `--name`, `--description`, `--author`, `--prefix`, `--no-ui`, `--out`, `--force`. |
| `p:extension:install {path}` | Install a `.pteroext` file, `.zip` file, or directory. Add `--enable` to enable it afterwards. |
| `p:extension:enable {id}` | Enable an extension, publish its frontend files, and run its migrations. |
| `p:extension:disable {id}` | Disable an extension, keeping its files and data. |
| `p:extension:remove {id}` | Delete an extension's files and published assets, keeping its tables and settings. |
| `p:extension:list` | List installed extensions and their state. |
| `p:extension:doctor {path}` | Check an unpacked package's manifest, compatibility, autoload directories, and built files without installing it. Warns about icon names this Panel does not ship and icon images it cannot serve. |
| `p:extension:dev {path}` | Build and publish once. Add `--enable` to enable the package and `--watch` to publish each successful rebuild. Browser reload requires `APP_DEBUG=true`. |
| `p:extension:types {path}` | Generate literal extension, screen, permission, and public configuration types. Defaults to `src/client/extension-types.ts`; change it with `--output`. Settings and permission definitions load only when the extension is enabled. |
| `p:extension:pack {path}` | Create a runtime `.pteroext` archive, including the image named by `icon`. Defaults to `{id}-{version}.pteroext` in the working directory. Options: `--output`, `--force`. |
## Backend [#backend]
### Provider Methods [#provider-methods]
`Pterodactyl\Extensions\ExtensionProvider` has these protected methods:
| Method | Description |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| `registerApiRoutes()` | Load `routes/client.php`, `routes/server.php`, `routes/admin.php`, and `routes/application.php`, if they exist. |
| `loadExtensionMigrations()` | Register `database/migrations`. |
| `registerSettings($definition)` | Register the extension's settings. |
| `settings()` | Return the extension's key-value settings store. |
| `registerPermissions($description, $keys)` | Register permission keys and their descriptions under `ext.{id}.{key}`. |
| `listenToServerOperations($listener)` | Listen for an immutable `OperationCompleted` result; errors are recorded against the extension. |
| `loadExtensionViews()` | Load Blade views from `resources/views`, as `ext-{id}::view`. |
| `loadExtensionTranslations()` | Load translations from `resources/lang`, as `ext-{id}::key`. |
| `id()` | Return the extension's ID. |
| `extensionPath(...$parts)` | Return an absolute path inside the extension's directory. |
### Route Helpers [#route-helpers]
Each helper loads a route file under a prefix, with the Panel's middleware. `registerApiRoutes` calls the first four.
| Method | URL prefix | Access |
| ------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `registerClientApiRoutes($path)` | `/api/client/extensions/{id}` | Signed-in users and Client API keys. |
| `registerServerApiRoutes($path)` | `/api/client/servers/{server}/extensions/{id}` | Users with access to the server. |
| `registerAdminApiRoutes($path)` | `/api/admin/extensions/{id}` | Root administrators. |
| `registerApplicationApiRoutes($path)` | `/api/application/extensions/{id}` | Root administrators, including Application API keys. |
| `registerAuthenticatedWebRoutes($path)` | `/extensions/{id}` | Signed-in users. |
| `registerWebRoutes($path)` | `/extensions/{id}` | Anyone. Add your own middleware to protect these routes. |
| `registerRootRoutes($path, $prefix = null)` | `/{prefix}`, for each prefix in `routes.root` or only `$prefix` | Anyone, rate limited per client by `extensions.root_routes_per_minute` (default 120). Undeclared prefixes are refused. |
Route names are prefixed with `extensions.{id}.`, followed by `client.`, `server.`, `admin.`, `application.`, `web.`, or `root.{prefix}.`. The path `/api/admin/extensions/{id}/settings` is reserved for the Panel.
### Settings [#settings]
`ExtensionSettingDefinition::make($key, $input, $default, $rules)` defines one setting, with these methods:
| Method | Description |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `label($label)` | The form label. Defaults to the input name in title case. |
| `help($text)` | Help text shown below the field. |
| `field($type, $options = [])` | The form control: `text`, `password`, `number`, `toggle`, or `select`. For `select`, pass a list of `['value' => ..., 'label' => ...]` options. |
| `frontend()` | Send the value to the frontend bundle as `config`, readable by every signed-in user. `config` is empty before sign-in. |
| `frontendType($type)` | Declare and check a public value's type: `string`, `number`, `boolean`, `array`, `object`, `null`, or `json`. Defaults to `json`. |
| `secret()` | Encrypt the stored value, use a password control, and return it blank to the admin form. Cannot be combined with `frontend()`. Empty updates keep the saved value. |
| `normalizeUsing($callback)` | Transform the value when it is read and saved. |
| `publicUsing($callback)` | Transform the value before it is shown, for example to mask it. |
The admin form validates submitted values against their definitions. Updates can be partial. Values must be JSON scalars or arrays, nested at most ten levels deep. A password control alone does not enable encryption.
The store returned by `settings()` supports:
| Method | Description |
| -------------------------------------- | --------------------------------------------------------------------- |
| `get($key, $default = null)` | Read a value, decrypting it when stored as a secret. |
| `set($key, $value)` | Store an ordinary JSON value. |
| `setSecret($key, $value)` | Store an encrypted value. Use it for every write to a managed secret. |
| `setMany($values)` | Store a map of ordinary values in one write. |
| `setManySecrets($values, $secretKeys)` | Store a map of values, encrypting the keys in `$secretKeys`. |
| `forget($key)` | Delete a value. |
| `all()` | Read all values in this scope. |
| `getByPrefix($prefix)` | Read matching keys, removing the prefix from the returned keys. |
| `forgetByPrefix($prefix)` | Delete matching keys. |
| `forUser($user)` | Return a store scoped to a persisted user. |
| `forServer($server)` | Return a store scoped to a persisted server. |
Global, user, and server scopes are separate. Deleting a user or server deletes its scoped settings. `ExtensionSettings::preload($stores)` loads several stores together. An `ExtensionSettingsDefinition` also supports `forUser` and `forServer`.
### Permissions [#permissions]
Register permissions with `registerPermissions($description, ['read' => 'Read notes.'])`. The key becomes `ext.{id}.read` and appears in the server subuser editor. Use it with `$user->can(...)`, the SDK permission hooks, and a server screen's `permission` list. A wildcard such as `ext.server-notes.*` covers only that extension. Server owners and root administrators pass every permission check.
You can also use these core permissions:
| Group | Permissions |
| --------- | ---------------------------------------------------------------------------------------------------------- |
| Console | `websocket.connect`, `control.console`, `control.start`, `control.stop`, `control.restart` |
| Subusers | `user.create`, `user.read`, `user.update`, `user.delete` |
| Files | `file.create`, `file.read`, `file.read-content`, `file.update`, `file.delete`, `file.archive`, `file.sftp` |
| Backups | `backup.create`, `backup.read`, `backup.delete`, `backup.download`, `backup.restore` |
| Network | `allocation.read`, `allocation.create`, `allocation.update`, `allocation.delete` |
| Startup | `startup.read`, `startup.update`, `startup.docker-image` |
| Databases | `database.create`, `database.read`, `database.update`, `database.delete`, `database.view_password` |
| Schedules | `schedule.create`, `schedule.read`, `schedule.update`, `schedule.delete` |
| Settings | `settings.rename`, `settings.reinstall` |
| Activity | `activity.read` |
### Events [#events]
The Panel dispatches these events from the `Pterodactyl\Events\Extensions` namespace. Each has a public `$manifest` property unless noted.
| Event | Dispatched |
| --------------------- | ------------------------------------------------------------------------------------------------------------ |
| `ExtensionInstalling` | Before an extension is installed. Also has `$source`. |
| `ExtensionInstalled` | After an extension is installed. |
| `ExtensionEnabling` | Before an extension is enabled. |
| `ExtensionEnabled` | After an extension is enabled and its migrations have run. |
| `ExtensionDisabling` | Before an extension is disabled. |
| `ExtensionDisabled` | After an extension is disabled. |
| `ExtensionRemoving` | Before an extension is removed. |
| `ExtensionRemoved` | After an extension is removed. Has only `$identifier`. |
| `ExtensionLoadFailed` | When an extension's provider throws while loading. Has `$identifier`, `$phase`, `$reason`, and `$exception`. |
### Server Operation Events [#server-operation-events]
`listenToServerOperations` receives `Pterodactyl\Events\Server\OperationCompleted`:
| Property | Value |
| -------------- | ------------------------------------------------------------------ |
| `serverUuid` | The server's UUID. |
| `operation` | `provision`, `install`, `reinstall`, or `backup`. |
| `successful` | Whether the operation succeeded. |
| `resourceUuid` | The backup UUID for backups; the server UUID for other operations. |
These properties are read-only. Events dispatch after the database transaction commits. Provisioning is reported only after Wings accepts creation. Listener failures are recorded against the extension.
### Job Progress [#job-progress]
`Pterodactyl\Services\Extensions\ExtensionJobProgress` supports:
| Method | Description |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `begin($extension, $user, $server = null, $permission = null)` | Create an `ExtensionJobSnapshot` with a UUID, `running` status, and zero percent. |
| `update($extension, $id, $percent, $message = '', $status = 'running')` | Update a snapshot. Percent must be 0–100 and cannot decrease; messages may be up to 1,000 characters. Completing sets percent to 100. |
| `find($extension, $id)` | Read a snapshot, or `null` if absent or expired. Performs no authorization. |
| `visible($extension, $id, $user, $server = null)` | Read a snapshot after checking its subject and the user's current access. Unavailable snapshots return 404. |
Status is `running`, `completed`, or `failed`. Finished snapshots cannot be updated. Each update increases `sequence`. The frontend payload contains `id`, `extension`, `status`, `percent`, `message`, `sequence`, and `updated_at`.
| GET endpoint | Subject |
| ------------------------------------------------------------------- | ---------------------- |
| `/api/client/extension-progress/{extension}/{job}` | A user-only job. |
| `/api/client/servers/{server}/extension-progress/{extension}/{job}` | A job for that server. |
Both endpoints require authentication. User-only jobs and server jobs without a permission are private to their creator and root administrators. Server jobs with a permission require current server access and that permission, which must belong to the same extension. Disabled extensions and missing, expired, or inaccessible jobs return 404. Browsers do not cache responses.
Workers and web requests must share a cache store. `extensions.progress_retention_seconds` sets retention, defaulting to 3,600 seconds after the last update. Store durable results or audit records elsewhere.
## Frontend SDK [#frontend-sdk]
Import runtime components and hooks from `@pterodactyl/sdk`. The Panel provides them through its import map, so they are not bundled into your extension. Build, testing, and generated-client helpers use the subpaths below.
Use `definePterodactylExtension({ setup })` for an entry definition, or `defineConfiguredExtension(parseConfig, { setup })` with generated configuration and screen types. `setup` is synchronous; its registrations take effect together once it succeeds.
### Setup Context [#setup-context]
`setup` receives an object with these properties:
| Property | Description |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `meta` | `{ id, version }` of your extension. |
| `config` | Your frontend setting values. |
| `slots.register(name, component)` | Render a component in a slot. |
| `screens.register(id, importer, options?)` | Attach a component to a screen declared in `extension.json`. The importer must return a module with a default export, such as `() => import('./Screen')`. `options.visible` is the predicate a `when.runtime` screen requires, and `options.badge` returns a live navigation badge. Both receive a `ScreenContext` with `user`, `config`, and the current `server` or `resource`. |
| `columns.register(name, column)` | Add a typed column to a supported admin table. |
| `components.replace(name, replacement)` | Register a presentation for a name declared in `ui.components`. Accepts a component or `{ load: () => import('./View') }`. |
### Component Replacements [#component-replacements]
The component catalog is exported as `COMPONENT_NAMES`. `ReplacementProps` contains a read-only `model`, the native `Default` component, and `parts`. `Default` accepts `className` and a partial `parts` map. Each part receives `{ model }`, typed by `ComponentPartProps`.
| Name | Model | Native parts |
| ---------------------- | ------------------ | ----------------------------------------- |
| `dashboard.serverCard` | `ServerCardModel` | `identity`, `address`, `metrics` |
| `server.files.details` | `FileDetailsModel` | `icon`, `name`, `size`, `modified` |
| `server.files.editor` | `FileEditorModel` | `notice`, `editor`, `language`, `actions` |
| `server.files.manager` | `FileManagerModel` | `toolbar`, `list`, `selection` |
`ServerCardModel` has `identifier`, `uuid`, `name`, nullable `description`, nullable `address`, and `state`. Its state is one of:
* `{ kind: 'loading' }`.
* `{ kind: 'unavailable', reason }`, where `reason` is `suspended`, `connection-error`, `maintenance`, `transferring`, `installing`, `restoring-backup`, or `unavailable`.
* `{ kind: 'ready', power, cpu, memory, disk }`. `power` is `offline`, `stopped`, `starting`, `running`, or `stopping`. Each metric has numeric `value`, numeric `limit`, and boolean `alarm`. CPU values are percentages; memory and disk values are bytes. A zero limit means unlimited.
`FileDetailsModel` has `name`, `kind`, `size`, and `modifiedAt`. `kind` is `file`, `directory`, `archive`, or `symlink`. Size is in bytes; `modifiedAt` is the API timestamp string.
`server.files.editor` replaces the editing surface of the file editor. `FileEditorModel` has `path`, `name`, `isNew`, `content`, `language`, `readOnly`, `dirty`, and `saving`, plus `change(content)`, `save()`, and `saveAs(name)`. For a new file, `path` is its directory and `name` is empty. `language` is a MIME type such as `text/x-yaml`. Call `change` after every edit; the Panel saves that text. `save` and `saveAs` resolve whether the file was saved.
`server.files.manager` replaces the file browser's toolbar, listing, and selection bar. `FileManagerModel` has `directory`, `entries`, `truncated`, `loading`, `refreshing`, `selection`, `permissions`, and `actions`. Each `FileManagerEntry` has `name`, `path`, `kind`, `size`, `mimetype`, `mode`, `modeBits`, `modifiedAt`, and `openable`, with at most 250 entries per directory. `permissions` has boolean `create`, `update`, `delete`, and `archive`. `actions` has `open`, `navigate`, `newFile`, `select`, `refresh`, `createDirectory`, `rename`, `remove`, `copy`, `archive`, `extract`, `chmod`, `download`, and `upload`, which run the Panel's own mutations. `remove` does not ask for confirmation. The file manager slots and the `server.files.details` replacement render only inside the native parts you keep.
The server card and file details are non-interactive content inside core links. The Panel keeps polling, permission-aware navigation, accessible link names, selection, menus, and modals. The native server-card `identity` part renders `dashboard.serverRow.name.after`; other server-row slots remain in the core frame.
Each declared name must have exactly one implementation in synchronous `setup`. Unknown names, undeclared registrations, duplicate registrations, or missing implementations fail the extension's atomic setup. Lazy importers return a module with a default React component. Use the importer form for lazy views instead of registering `React.lazy`.
A component has one enabled owner. Conflicting enable and update requests fail before activation. Enabling or disabling the extension toggles its replacements; reload open browser sessions to apply the change. Conflicting packages found at startup are skipped, with errors naming their claims.
Rows share one loading decision per page and component name. Loading has a five-second deadline; a timed-out implementation cannot replace the native view later on that page. Import and render failures restore the native view. Core state stays above the presentation boundary; extension-local state can be lost on failure. An uncontained suspension commits that mount to its native view; use local `Suspense` for extension-owned asynchronous content. Event and asynchronous callback failures need `useExtensionAction`, separate from render boundaries.
See [Component Replacements](./frontend.mdx#component-replacements) for registration and testing examples.
### Hooks [#hooks]
| Hook | Returns |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `useCurrentUser()` | The signed-in user. |
| `useSiteSettings()` | The Panel's name, locale, and reCAPTCHA settings. |
| `useCurrentServer(select?)` | The current server on server pages, or `undefined` elsewhere. |
| `useCurrentServerRequired(message?)` | The current server. Throws on pages without a server. |
| `useCurrentServerUuid()` | The current server's UUID, or `undefined`. |
| `useCurrentServerPermissions()` | The current user's permissions on the current server. `['*']` for the owner. |
| `useServerPermission(permission, matchAny?)` | Whether the current user has the permission, or every permission in a list. Set `matchAny` to `true` to require any one. |
| `usePermissions(permissions)` | One boolean per permission. |
| `useServerWebsocketEvent(event, callback)` | Calls `callback` for typed live server events, such as `SocketEvent.STATUS`, `SocketEvent.CONSOLE_OUTPUT`, or `backup completed:{uuid}`. Server pages only; callback failures are attributed to the extension. |
| `useCurrentResource()` | `{ kind, resource }` on an admin resource detail page, or `null` elsewhere. Check `kind` before reading resource-specific fields. |
| `useExtensionAction(name, callback)` | A wrapped handler whose failures are reported against the extension. Use it for asynchronous actions in screens and slots. |
| `useExtensionTranslation(group = 'messages')` | `{ locale, ready, t }` for a registered `resources/lang/{locale}/{group}.php` group. Call inside an extension screen or slot. |
| `useExtensionJobProgress(job, { serverUuid }?)` | A query result for the extension's job. Pass a server UUID for server jobs. Polls running work every 1.5 seconds, refetches after reconnecting, and stops on completion, failure, or an access error. |
### Server Data [#server-data]
These factories return query options that use the Panel's transport and cache. Pass the server UUID for files, startup, and backups to share the native page's cache entries.
| Factory | Data |
| ------------------------------------------- | ------------------------------------------ |
| `serverQueryOptions(id)` | A server lookup. |
| `serverFilesQueryOptions(uuid, directory)` | The file listing for a directory. |
| `serverFileContentQueryOptions(uuid, file)` | A file's contents as a string. |
| `serverStartupQueryOptions(uuid)` | Startup variables and image configuration. |
| `serverBackupsQueryOptions(uuid, page = 1)` | A page of backups. |
| Hook | Data |
| ------------------------------------------- | --------------------------------------------------- |
| `useServerFiles(uuid, directory, select?)` | File listing query result; optional typed selector. |
| `useServerFileContent(uuid, file)` | File content query result. |
| `useServerStartup(uuid, select?)` | Startup query result; optional typed selector. |
| `useServerBackups(uuid, page = 1, select?)` | Backup query result; optional typed selector. |
`invalidateServerData(uuid, domains)` invalidates the native cache and returns a promise. Domains are `server`, `files`, `fileContent`, `startup`, and `backups`. File and backup invalidation covers all cached directories or pages for that server. Await it when your own mutation changes native data.
### Mutations [#mutations]
Each hook returns `isPending`, `error`, `data`, `mutateAsync(input)`, and `reset()`. Await `mutateAsync` from a guarded action handler. The hooks apply native cache updates and invalidation; startup writes are serialized per server.
| Hook | Input |
| -------------------------------------- | ------------------------------------------------------------ |
| `useCreateServerDirectory(uuid)` | `{ directory, name }` |
| `useRenameServerFiles(uuid)` | `{ directory, files: [{ from, to }] }` |
| `useDeleteServerFiles(uuid)` | `{ directory, files: string[] }` |
| `useWriteServerFile(uuid)` | `{ file, content }` |
| `useUpdateServerStartupVariable(uuid)` | `{ key, value }`; returns the updated variable. |
| `useSetServerDockerImage(uuid)` | `{ image }` |
| `useCreateServerBackup(uuid)` | `{ name?, ignored?, isLocked }`; returns the created backup. |
The hooks do not replace backend authorization. Check the user's permission before showing a write control.
### Navigation [#navigation]
| Export | Description |
| -------------------------------------- | -------------------------------------------------------------------------- |
| `PanelLink` | A link with a `destination` and optional `exact`. Uses the Panel router. |
| `usePanelNavigate()` | A function taking a destination and returning a navigation promise. |
| `usePanelLocation()` | The current `RouteSlotData`. |
| `resolvePanelDestination(destination)` | Resolve a destination to its `pathname`, search, hash, and replace option. |
A core destination is `{ to, params?, search?, hash?, replace? }`. `to` must be a supported core route; TypeScript checks required route parameters. An extension destination is `{ extension, screen, params?, search?, hash?, replace? }`; pass the declared screen ID and the parameters needed for its screen and resource parent.
### Table Columns [#table-columns]
`columns.register(name, { id, label, component })` supports these tables:
| Name | Component data |
| --------------- | --------------------------- |
| `admin.nodes` | The native node resource. |
| `admin.servers` | The native server resource. |
| `admin.eggs` | The native egg resource. |
The component receives `{ data }`, typed as `ExtensionTableRows[Name]`. Column IDs use lowercase letters, numbers, and hyphens, starting with a letter, up to 48 characters. Columns follow extension order and render after core columns. Each cell has an error boundary.
### Components [#components]
| Component | Description |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `Button` | A button. Also `Button.Text` and `Button.Danger`. Supports `isLoading`, `size`, `color`, and `isSecondary`. |
| `Input`, `TextArea` | Text fields. |
| `Select` | A select menu, with single or multiple values. |
| `Checkbox`, `Switch` | Checkboxes and switches. |
| `Label` | A form label. |
| `Form`, `useAppForm` | Forms built on TanStack Form, with ready-made field components. |
| `Dialog` | Dialogs, including `Dialog.Confirm`. |
| `Alert` | A notice, with type `success`, `info`, `warning`, or `danger`. |
| `ContentBox`, `TitledGreyBox` | Content boxes. |
| `PageContentBlock`, `ServerContentBlock` | Page wrappers that set the page title. Use `ServerContentBlock` on server screens. |
| `Spinner` | A loading indicator. |
| `Icon` | A [Lucide](https://lucide.dev) icon. |
| `Can` | Renders its children only if the user has a permission. |
| `CopyOnClick` | Copies text when clicked. |
| `ScreenBlock`, `ServerError`, `NotFound` | Full-page messages. |
| `Table` | A native table with `rows`, `columns`, `keyOf`, and `emptyState`. Each column has `id`, `label`, and a `cell(row)` function. |
| `Tooltip` | A themed tooltip with `content`, an element child, and optional `placement`. |
| `DropdownMenu`, `DropdownMenuItem`, `ContextDropdownMenu` | Dropdown and context menus. |
| `Pagination` | A native page selector. |
| `PanelLink` | A link using a typed core or extension destination. |
The SDK also exports `toast` for notifications.
### HTTP [#http]
| Export | Description |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| `http` | The Panel's Axios instance, already signed in. |
| `httpErrorToHuman(error)` | Turns an API error into a readable message. |
| `queryClient` | The Panel's TanStack Query client. |
| `client` from `@pterodactyl/sdk/api` | The shared transport used by generated extension clients. |
| `extensionProgressQueryOptions(extension, job, serverUuid?)` | Query options keyed by extension, job, and subject. The progress endpoint enforces authorization. |
### Types [#types]
| Type | Description |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `SdkServer` | A server from the Client API, with fields under `attributes`. |
| `SdkUser` | The signed-in user. |
| `RouteSlotData` | `{ pathname, search, params }` for the current page. |
| `SubuserPermissionsSlotData` | The data passed to the subuser permission editor slot. |
| `ExtensionConfig` | The type of `config`. |
| `ExtensionConfigValue` | A JSON value in frontend configuration. |
| `ScreenComponentProps` | Route data including named parameters, with an optional typed `resource` for detail screens. |
| `ExtensionResourceContext` | The typed admin resource union with `kind` and `resource`. |
| `FileManagerSlotData`, `FileRowSlotData` | Native file context, selection, and callbacks; the row contract also has `file`. |
| `StartupFormSlotData` | Startup configuration and native callbacks. |
| `AdminUserFormSlotData`, `AdminUserFormValues` | The native user resource, bound form, and its existing field names. |
| `ExtensionTableRows`, `ExtensionTableName`, `ExtensionTableColumn` | Native table row and column contracts. |
| `ComponentName`, `ComponentModels`, `ReplacementProps`, `ComponentPartProps`, `ComponentParts`, `DefaultComponentProps` | Component replacement names, models, props, native parts, and default-view props. |
| `ServerCardModel`, `ServerCardState`, `ServerCardMetric`, `FileDetailsModel`, `FileEditorModel`, `FileManagerModel`, `FileManagerEntry`, `FileManagerPermissions`, `FileManagerActions` | Read-only presentation models. |
| `ScreenCondition`, `ScreenMatcher`, `ScreenOptions`, `ScreenContext`, `ScreenBadgeValue` | Screen `when` conditions and the `visible` and `badge` options. |
| `ReplacementImporter`, `ComponentReplacement` | Direct component and lazy importer registration types. |
| `SdkFile`, `SdkBackup`, `SdkServerFiles`, `SdkServerStartup`, `SdkServerBackups` | The native server resource response types. |
| `SdkMutation`, `SdkFileSelection` | Mutation state and file selection input. |
| `PanelDestination`, `PanelLinkProps` | Core and extension navigation contracts. |
| `ServerWebsocketEvent` | Known server events and backup-specific completion event names. |
| `ExtensionJobProgress` | The public job snapshot payload. |
### Build Configuration [#build-configuration]
Build your extension with `defineExtensionConfig` from `@pterodactyl/sdk/vite`:
```js
import { defineExtensionConfig } from '@pterodactyl/sdk/vite';
export default defineExtensionConfig({ entry: 'src/client/index.tsx' });
```
Options: `entry` (required), `outDir` (default `dist`), and `plugins`.
### Testing and Styling [#testing-and-styling]
| Package entry | Exports or use |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@pterodactyl/sdk/testing` | `createExtensionTestHost(options?)`, `createTestServer(options?)`, `createComponentTestHost(name, options)`, `createTestServerCard(options?)`, `createTestFileDetails(options?)`, `createTestFileEditor(options?)`, `createTestFileManager(options?)`, and `createTestFileManagerEntry(options?)`. |
| `@pterodactyl/sdk/vitest` | `pterodactylTestAliases`, `pterodactylTestSetup`, and `pterodactylTestDirectory` for Vitest configuration. |
| `@pterodactyl/sdk/openapi` | `externalizeExtensionApi(directory)` connects generated code to the shared transport. The scaffold calls it after API generation. |
| `@pterodactyl/sdk/theme.css` | Tailwind theme mappings to the Panel's design tokens. Import with theme and utilities; omit preflight. |
| `@pterodactyl/sdk/manifest.schema.json` | Editor completion for `extension.json`. |
Test host options are `extensionId`, `prefix` (your `ui.prefix`), `path`, `user`, `siteSettings`, `server`, and a typed admin `resource`. The returned host has `Wrapper`, `queryClient`, `navigate(destination)`, `emitWebsocket(event, data)`, `setConnected(connected)`, and `dispose()`. Use one host per test environment at a time; unmount its wrapper before disposing it.
`createTestServer` accepts `identifier`, `uuid`, `name`, `owner`, `permissions`, and `status`. In a source checkout, build the test runtime with `npm run sdk:testing` from the Panel root.
`createComponentTestHost` accepts a supported component name and `{ model, extensionId?, prefix? }`, and returns `Wrapper`, typed replacement `props`, and `dispose()`. Call `dispose()` after unmounting the wrapper. Its defaults and parts are the Panel's native implementations. The model fixture helpers accept partial model overrides.
Frontend scaffolds include tests, CI, PostCSS, a stylesheet, and OpenAPI generation. Their `npm run api:generate` expects an extension `openapi.yaml`. To generate it, load the extension's routes, then run `composer docs:openapi` and `npm run extension:api:generate -- {id}` from the Panel root.
Entry CSS loads before `setup`; imported screen styles load with their screen chunk. Scope custom selectors to the extension. See [Components and Styling](./data-and-styling.mdx#components-and-styling).
## Slots [#slots]
Slots are named places on Panel pages where extensions render components. Register a component with `slots.register(name, component)`. The `data` column shows the component's `data` prop.
### Slot Data [#slot-data]
| Contract | Fields and use |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `RouteSlotData` | `pathname`, `search`, and `params`. |
| `SdkServer` | The current server, with resource fields under `attributes`. |
| `SubuserPermissionsSlotData` | `mode`, `selectedPermissions`, `editablePermissions`, `disabled`, and `setPermissions`. |
| `FileManagerSlotData` | `server`, `directory`, `files`, `selectedFiles`, `isFetching`, `setSelectedFiles(names)`, and `refresh()`. |
| `FileRowSlotData` | The file manager fields, plus the current `file`. |
| `StartupFormSlotData` | `server`, `configuration`, `isPending`, `canChangeDockerImage`, `setDockerImage(image)`, and `refresh()`. |
| `ExtensionResourceContext` | `kind` and the matching native `resource`. Detail action slots narrow this to their own resource kind. |
| `AdminUserFormSlotData` | `kind: 'admin.user'`, `resource`, and the native `form`. Use `form.AppField` for existing user fields. |
The native user form fields are `email`, `username`, `nameFirst`, `nameLast`, `password`, `rootAdmin`, and `language`. Contributed controls share validation, pending state, submission, and persistence with the Panel's form. Extension-specific values need scoped settings or an extension endpoint.
### Navigation Slots [#navigation-slots]
| Slot | Location | Position | `data` |
| ------------------ | ------------------ | -------- | ------ |
| `nav.items.before` | Top navigation bar | Before | None |
| `nav.items.after` | Top navigation bar | After | None |
### Authentication Slots [#authentication-slots]
| Slot | Location | Position | `data` |
| --------------------------- | ---------------------------------- | -------- | --------------- |
| `auth.login.before` | Login page | Before | `RouteSlotData` |
| `auth.login.after` | Login page | After | `RouteSlotData` |
| `auth.login.form.after` | Login form, below the Login button | After | `RouteSlotData` |
| `auth.checkpoint.before` | Two-factor checkpoint page | Before | `RouteSlotData` |
| `auth.checkpoint.after` | Two-factor checkpoint page | After | `RouteSlotData` |
| `auth.password.before` | Forgot password page | Before | `RouteSlotData` |
| `auth.password.after` | Forgot password page | After | `RouteSlotData` |
| `auth.passwordReset.before` | Password reset page | Before | `RouteSlotData` |
| `auth.passwordReset.after` | Password reset page | After | `RouteSlotData` |
### Dashboard Slots [#dashboard-slots]
| Slot | Location | Position | `data` |
| ----------------------------------- | --------------------------------------------- | -------- | ----------- |
| `dashboard.before` | Dashboard | Before | None |
| `dashboard.after` | Dashboard | After | None |
| `dashboard.serverRow.before` | Each server on the dashboard | Before | `SdkServer` |
| `dashboard.serverRow.after` | Each server on the dashboard | After | `SdkServer` |
| `dashboard.serverRow.name.after` | Each server's name on the dashboard | After | `SdkServer` |
| `dashboard.serverRow.metrics.after` | Each server's resource usage on the dashboard | After | `SdkServer` |
### Account Slots [#account-slots]
| Slot | Location | Position | `data` |
| --------------------------- | ------------------ | -------- | --------------- |
| `account.navigation.before` | Account navigation | Before | None |
| `account.navigation.after` | Account navigation | After | None |
| `account.overview.before` | Account overview | Before | None |
| `account.overview.after` | Account overview | After | None |
| `account.api.before` | API keys page | Before | `RouteSlotData` |
| `account.api.after` | API keys page | After | `RouteSlotData` |
| `account.ssh.before` | SSH keys page | Before | `RouteSlotData` |
| `account.ssh.after` | SSH keys page | After | `RouteSlotData` |
| `account.activity.before` | Activity page | Before | `RouteSlotData` |
| `account.activity.after` | Activity page | After | `RouteSlotData` |
### Server Slots [#server-slots]
| Slot | Location | Position | `data` |
| --------------------------------- | ------------------------- | -------- | ---------------------------- |
| `server.navigation.before` | Server navigation | Before | `SdkServer` |
| `server.navigation.after` | Server navigation | After | `SdkServer` |
| `server.console.before` | Console page | Before | `SdkServer` |
| `server.console.power.before` | Console power buttons | Before | `SdkServer` |
| `server.console.power.after` | Console power buttons | After | `SdkServer` |
| `server.console.after` | Console page | After | `SdkServer` |
| `server.files.before` | File manager | Before | None |
| `server.files.after` | File manager | After | None |
| `server.files.toolbar` | File manager toolbar | Inside | `FileManagerSlotData` |
| `server.files.rowActions` | File row and context menu | Actions | `FileRowSlotData` |
| `server.files.selectionActions` | Selected-file action bar | Actions | `FileManagerSlotData` |
| `server.files.editor.before` | File editor | Before | `RouteSlotData` |
| `server.files.editor.after` | File editor | After | `RouteSlotData` |
| `server.databases.before` | Databases page | Before | `RouteSlotData` |
| `server.databases.after` | Databases page | After | `RouteSlotData` |
| `server.schedules.before` | Schedules page | Before | `RouteSlotData` |
| `server.schedules.after` | Schedules page | After | `RouteSlotData` |
| `server.schedules.detail.before` | Schedule details page | Before | `RouteSlotData` |
| `server.schedules.detail.after` | Schedule details page | After | `RouteSlotData` |
| `server.users.before` | Users page | Before | `RouteSlotData` |
| `server.users.after` | Users page | After | `RouteSlotData` |
| `server.users.create.before` | Create subuser page | Before | `RouteSlotData` |
| `server.users.create.after` | Create subuser page | After | `RouteSlotData` |
| `server.users.permissions.before` | Subuser permission editor | Before | `SubuserPermissionsSlotData` |
| `server.backups.before` | Backups page | Before | `RouteSlotData` |
| `server.backups.after` | Backups page | After | `RouteSlotData` |
| `server.network.before` | Network page | Before | `RouteSlotData` |
| `server.network.after` | Network page | After | `RouteSlotData` |
| `server.startup.before` | Startup page | Before | `RouteSlotData` |
| `server.startup.after` | Startup page | After | `RouteSlotData` |
| `server.startup.form` | Startup configuration | Inside | `StartupFormSlotData` |
| `server.settings.before` | Settings page | Before | `RouteSlotData` |
| `server.settings.after` | Settings page | After | `RouteSlotData` |
| `server.activity.before` | Activity page | Before | `RouteSlotData` |
| `server.activity.after` | Activity page | After | `RouteSlotData` |
### Admin Slots [#admin-slots]
| Slot | Location | Position | `data` |
| ----------------------------------------- | -------------------------- | -------- | ------------------------------- |
| `panel.navigation.before` | Admin navigation | Before | None |
| `panel.navigation.after` | Admin navigation | After | None |
| `panel.overview.before` | Admin overview | Before | None |
| `panel.overview.after` | Admin overview | After | None |
| `panel.users.before` | Users list | Before | `RouteSlotData` |
| `panel.users.after` | Users list | After | `RouteSlotData` |
| `panel.users.create.before` | Create user page | Before | `RouteSlotData` |
| `panel.users.create.after` | Create user page | After | `RouteSlotData` |
| `panel.users.detail.before` | User details page | Before | `RouteSlotData` |
| `panel.users.detail.after` | User details page | After | `RouteSlotData` |
| `panel.users.detail.actions` | User detail header | Actions | `admin.user` resource context |
| `panel.users.detail.form` | Native user form | Inside | `AdminUserFormSlotData` |
| `panel.locations.before` | Locations list | Before | `RouteSlotData` |
| `panel.locations.after` | Locations list | After | `RouteSlotData` |
| `panel.locations.detail.before` | Location details page | Before | `RouteSlotData` |
| `panel.locations.detail.after` | Location details page | After | `RouteSlotData` |
| `panel.nodes.before` | Nodes list | Before | `RouteSlotData` |
| `panel.nodes.after` | Nodes list | After | `RouteSlotData` |
| `panel.nodes.create.before` | Create node page | Before | `RouteSlotData` |
| `panel.nodes.create.after` | Create node page | After | `RouteSlotData` |
| `panel.nodes.detail.before` | Node details page | Before | `RouteSlotData` |
| `panel.nodes.detail.after` | Node details page | After | `RouteSlotData` |
| `panel.nodes.detail.actions` | Node detail header | Actions | `admin.node` resource context |
| `panel.nodes.detail.about.before` | Node About tab | Before | `RouteSlotData` |
| `panel.nodes.detail.about.after` | Node About tab | After | `RouteSlotData` |
| `panel.nodes.detail.settings.before` | Node Settings tab | Before | `RouteSlotData` |
| `panel.nodes.detail.settings.after` | Node Settings tab | After | `RouteSlotData` |
| `panel.nodes.detail.configuration.before` | Node Configuration tab | Before | `RouteSlotData` |
| `panel.nodes.detail.configuration.after` | Node Configuration tab | After | `RouteSlotData` |
| `panel.nodes.detail.allocations.before` | Node Allocations tab | Before | `RouteSlotData` |
| `panel.nodes.detail.allocations.after` | Node Allocations tab | After | `RouteSlotData` |
| `panel.nodes.detail.servers.before` | Node Servers tab | Before | `RouteSlotData` |
| `panel.nodes.detail.servers.after` | Node Servers tab | After | `RouteSlotData` |
| `panel.servers.before` | Servers list | Before | `RouteSlotData` |
| `panel.servers.after` | Servers list | After | `RouteSlotData` |
| `panel.servers.create.before` | Create server page | Before | `RouteSlotData` |
| `panel.servers.create.after` | Create server page | After | `RouteSlotData` |
| `panel.servers.detail.before` | Server details page | Before | `RouteSlotData` |
| `panel.servers.detail.after` | Server details page | After | `RouteSlotData` |
| `panel.servers.detail.actions` | Server detail header | Actions | `admin.server` resource context |
| `panel.servers.detail.about.before` | Server About tab | Before | `RouteSlotData` |
| `panel.servers.detail.about.after` | Server About tab | After | `RouteSlotData` |
| `panel.servers.detail.details.before` | Server Details tab | Before | `RouteSlotData` |
| `panel.servers.detail.details.after` | Server Details tab | After | `RouteSlotData` |
| `panel.servers.detail.build.before` | Server Build tab | Before | `RouteSlotData` |
| `panel.servers.detail.build.after` | Server Build tab | After | `RouteSlotData` |
| `panel.servers.detail.startup.before` | Server Startup tab | Before | `RouteSlotData` |
| `panel.servers.detail.startup.after` | Server Startup tab | After | `RouteSlotData` |
| `panel.servers.detail.databases.before` | Server Databases tab | Before | `RouteSlotData` |
| `panel.servers.detail.databases.after` | Server Databases tab | After | `RouteSlotData` |
| `panel.servers.detail.mounts.before` | Server Mounts tab | Before | `RouteSlotData` |
| `panel.servers.detail.mounts.after` | Server Mounts tab | After | `RouteSlotData` |
| `panel.servers.detail.manage.before` | Server Manage tab | Before | `RouteSlotData` |
| `panel.servers.detail.manage.after` | Server Manage tab | After | `RouteSlotData` |
| `panel.servers.detail.delete.before` | Server Delete tab | Before | `RouteSlotData` |
| `panel.servers.detail.delete.after` | Server Delete tab | After | `RouteSlotData` |
| `panel.databaseHosts.before` | Database hosts list | Before | `RouteSlotData` |
| `panel.databaseHosts.after` | Database hosts list | After | `RouteSlotData` |
| `panel.databaseHosts.create.before` | Create database host page | Before | `RouteSlotData` |
| `panel.databaseHosts.create.after` | Create database host page | After | `RouteSlotData` |
| `panel.databaseHosts.detail.before` | Database host details page | Before | `RouteSlotData` |
| `panel.databaseHosts.detail.after` | Database host details page | After | `RouteSlotData` |
| `panel.mounts.before` | Mounts list | Before | `RouteSlotData` |
| `panel.mounts.after` | Mounts list | After | `RouteSlotData` |
| `panel.mounts.create.before` | Create mount page | Before | `RouteSlotData` |
| `panel.mounts.create.after` | Create mount page | After | `RouteSlotData` |
| `panel.mounts.detail.before` | Mount details page | Before | `RouteSlotData` |
| `panel.mounts.detail.after` | Mount details page | After | `RouteSlotData` |
| `panel.eggs.before` | Eggs list | Before | `RouteSlotData` |
| `panel.eggs.after` | Eggs list | After | `RouteSlotData` |
| `panel.eggs.create.before` | Create egg page | Before | `RouteSlotData` |
| `panel.eggs.create.after` | Create egg page | After | `RouteSlotData` |
| `panel.eggs.detail.before` | Egg details page | Before | `RouteSlotData` |
| `panel.eggs.detail.after` | Egg details page | After | `RouteSlotData` |
| `panel.eggs.detail.actions` | Egg detail header | Actions | `admin.egg` resource context |
| `panel.eggs.detail.configuration.before` | Egg Configuration tab | Before | `RouteSlotData` |
| `panel.eggs.detail.configuration.after` | Egg Configuration tab | After | `RouteSlotData` |
| `panel.eggs.detail.tags.before` | Egg Tags tab | Before | `RouteSlotData` |
| `panel.eggs.detail.tags.after` | Egg Tags tab | After | `RouteSlotData` |
| `panel.eggs.detail.variables.before` | Egg Variables tab | Before | `RouteSlotData` |
| `panel.eggs.detail.variables.after` | Egg Variables tab | After | `RouteSlotData` |
| `panel.eggs.detail.script.before` | Egg Install script tab | Before | `RouteSlotData` |
| `panel.eggs.detail.script.after` | Egg Install script tab | After | `RouteSlotData` |
| `panel.tags.before` | Tags page | Before | `RouteSlotData` |
| `panel.tags.after` | Tags page | After | `RouteSlotData` |
| `panel.activity.before` | Admin activity page | Before | `RouteSlotData` |
| `panel.activity.after` | Admin activity page | After | `RouteSlotData` |
| `panel.settings.before` | Settings pages | Before | `RouteSlotData` |
| `panel.settings.after` | Settings pages | After | `RouteSlotData` |
| `panel.extensions.before` | Extensions page | Before | `RouteSlotData` |
| `panel.extensions.after` | Extensions page | After | `RouteSlotData` |
| `panel.apiKeys.before` | API keys page | Before | `RouteSlotData` |
| `panel.apiKeys.after` | API keys page | After | `RouteSlotData` |
# Testing and Packaging
Test the extension before you share it, then build and package it.
## Testing [#testing]
Frontend scaffolds include Vitest, jsdom, Testing Library, and a test against the real SDK runtime. Run the checks inside the extension directory:
```bash
npm run typecheck
npm test
```
For components that need the Panel's router, current user, permissions, server, or query cache, use `createExtensionTestHost` and `createTestServer` from `@pterodactyl/sdk/testing`. For the Notes screen, mock the extension's API response and provide a server context:
```tsx title="src/client/screens/NotesScreen.spec.tsx"
import { afterEach, expect, test, vi } from 'vitest';
import { cleanup, render, screen } from '@testing-library/react';
import { createExtensionTestHost, createTestServer } from '@pterodactyl/sdk/testing';
import NotesScreen from './NotesScreen';
vi.mock('../api', async (importOriginal) => ({
...(await importOriginal()),
getNote: vi.fn().mockResolvedValue({ body: 'Remember to make a backup.', updated_at: null }),
}));
let host: ReturnType | undefined;
afterEach(() => {
cleanup();
host?.dispose();
host = undefined;
});
test('loads notes inside the Panel', async () => {
host = createExtensionTestHost({
extensionId: 'server-notes',
server: createTestServer({ owner: false, permissions: ['file.update'] }),
});
render( , { wrapper: host.Wrapper });
expect(await screen.findByDisplayValue('Remember to make a backup.')).toBeTruthy();
});
```
Create one host per test, then unmount its wrapper and dispose of it afterwards. The host also supports a typed admin resource, navigation, websocket events, and connection changes. The scaffold's `vitest.config.mjs` supplies SDK aliases and setup through `@pterodactyl/sdk/vitest`; keep them when you customize the configuration.
Backend routes and jobs need their own tests for validation and authorization. The generated GitHub Actions workflow runs frontend type checks, tests, and builds.
## Building and Packaging [#building-and-packaging]
Build the frontend. The build writes `dist/client.js` and a file for each screen:
```bash
npm run build
```
From the Panel root, check the built package before installing it:
```bash
php artisan p:extension:doctor extensions/server-notes
```
This checks the manifest, runtime and extension dependencies, autoload directories, and built frontend files, and reports errors without installing the package. It also warns about an `icon` the extensions page cannot show: a Lucide name this Panel does not ship, or an image that is missing, too large, or not a PNG, JPEG, or WebP file.
### Watching Changes [#watching-changes]
To build, publish, and enable a local extension, run the development command from the Panel root:
```bash
php artisan p:extension:dev extensions/server-notes --watch --enable
```
It runs an initial build, then runs the extension's `npm run dev` and republishes each successful build. A failed build keeps the last published version. With `APP_DEBUG=true`, open the Panel after starting the watcher, and the browser reloads after each successfully published build. Press Ctrl+C to stop the watcher. Without `--watch`, the command builds and publishes once.
### Creating a Package [#creating-a-package]
To create a `.pteroext` archive, run:
```bash
php artisan p:extension:pack extensions/server-notes --output=server-notes.pteroext
```
The archive contains the manifest, the image named by `icon`, and runtime directories: routes, migrations, resources, built assets, vendor dependencies, and declared PHP autoload directories. It excludes dotfiles, symlinks, `node_modules`, and test or coverage directories. An existing archive is kept unless you pass `--force`.
Check the package's contents before distributing it. Put custom runtime files in a directory the packager includes. Files under the PHP autoload directories, including `src/client`, are included unless the rules above exclude them.
Administrators install this file as described in [Installing Extensions](./index.mdx#installing-extensions).
## Security [#security]
Your extension runs with the same access as the Panel. Treat it like Panel code:
* Validate every request, and check permissions before changing data.
* Never expose secrets as frontend settings; every signed-in user can read public configuration.
* Declare secret settings with `secret()`, or use `setSecret` for managed values.
* Prefix your database tables so they cannot clash with the Panel's tables.
Declare and test the Panel, SDK, PHP, and extension versions your package supports. Compatibility constraints check versions; they do not replace tests of your extension's behavior.
## Example Extension [#example-extension]
The Panel repository includes a frontend-only example in `packages/hello-world`. It shows slots, a server page, live server events, and the subuser permission editor slot.
# Additional Configuration
## Backups [#backups]
The backup driver in the Panel's `.env` file sets where server backups are stored.
After you change drivers, users can still download and delete backups made with the previous one. If you move from S3 to local backups, keep your S3 settings in `.env` so older backups stay available.
### Local Backups [#local-backups]
By default, Wings stores backups on each node. To set this explicitly, add to `.env`:
```bash
APP_BACKUP_DRIVER=wings
```
Wings stores them in the `backup_directory` set in its `config.yml`:
```yml
system:
backup_directory: /path/to/backup/storage
```
### S3 Backups [#s3-backups]
To store backups in Amazon S3 or S3-compatible storage, add to `.env`:
```bash
APP_BACKUP_DRIVER=s3
AWS_DEFAULT_REGION=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_BACKUPS_BUCKET=
AWS_ENDPOINT=
```
For services that need path-style addresses, such as `domain.com/bucket` instead of `bucket.domain.com`, also add:
```bash
AWS_USE_PATH_STYLE_ENDPOINT=true
```
#### Multipart Uploads [#multipart-uploads]
Backups upload to S3 in parts of up to 5 GB, and each upload link is valid for 60 minutes. To change these, set `BACKUP_MAX_PART_SIZE` in bytes and `BACKUP_PRESIGNED_URL_LIFESPAN` in minutes. For 1 GB parts and 120-minute links:
```bash
BACKUP_MAX_PART_SIZE=1073741824
BACKUP_PRESIGNED_URL_LIFESPAN=120
```
#### Storage Class [#storage-class]
To use a different S3 storage class, set `AWS_BACKUPS_STORAGE_CLASS`. The default is `STANDARD`:
```bash
AWS_BACKUPS_STORAGE_CLASS=STANDARD_IA
```
## Reverse Proxies [#reverse-proxies]
Behind a reverse proxy, such as Cloudflare, NGINX, Apache, or Caddy, the Panel must trust the proxy. Otherwise it thinks it is served over HTTP instead of HTTPS, and you may be unable to sign in.
Set `TRUSTED_PROXIES` in `.env` to your proxies' IP addresses, separated by commas. For a proxy on the same machine:
```bash
TRUSTED_PROXIES=127.0.0.1
```
`TRUSTED_PROXIES=*` trusts every proxy, but we recommend listing the addresses.
### NGINX [#nginx]
When NGINX is the proxy, its `location` block must pass these headers to the Panel:
```nginx
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering off;
proxy_request_buffering off;
```
### Cloudflare [#cloudflare]
With Cloudflare's proxy, set `TRUSTED_PROXIES` to [Cloudflare's IP ranges](https://www.cloudflare.com/ips/):
```bash
TRUSTED_PROXIES=173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22
```
## reCAPTCHA [#recaptcha]
Invisible reCAPTCHA protects the login page from brute-force attacks. Suspicious login attempts may get a reCAPTCHA challenge.
### Using Your Own Keys [#using-your-own-keys]
The Panel ships with shared reCAPTCHA keys. Create your own in the [reCAPTCHA admin console](https://www.google.com/recaptcha/admin), then enter them under **Admin → Settings → Advanced**, in **reCAPTCHA Website Key** and **reCAPTCHA Secret Key**.
### Turning Off reCAPTCHA [#turning-off-recaptcha]
We do not recommend turning off reCAPTCHA. It makes brute-force attacks on user accounts harder.
If users have trouble signing in, or your Panel is not reachable from the internet, turn off **reCAPTCHA Enabled** under **Admin → Settings → Advanced**.
If you cannot sign in, change it in the database. Open the database console:
```bash
mariadb -u root
```
Then run:
```sql
INSERT INTO panel.settings (`key`, value) VALUES ('settings::recaptcha:enabled', 'false')
ON DUPLICATE KEY UPDATE value = 'false';
```
## Two-Factor Authentication [#two-factor-authentication]
To require two-factor authentication for every user, set **Require 2-Factor Authentication** under **Admin → Settings → General**.
### Removing the Requirement Without the Panel [#removing-the-requirement-without-the-panel]
If you cannot sign in to change it, open the database console with `mariadb -u root` and run:
```sql
INSERT INTO panel.settings (`key`, value) VALUES ('settings::pterodactyl:auth:2fa_required', 0)
ON DUPLICATE KEY UPDATE value = 0;
```
### Turning Off Two-Factor Authentication for a User [#turning-off-two-factor-authentication-for-a-user]
If a user loses their authenticator, run this from `/var/www/pterodactyl` and enter their email address when asked:
```bash
php artisan p:user:disable2fa
```
## Telemetry [#telemetry]
Once a day, the Panel sends anonymous statistics about itself and its nodes to the Pterodactyl team. Telemetry is on by default.
The data is not sold or used for advertising. We may publish combined statistics, or share them with others, to help improve Pterodactyl.
### What Is Collected [#what-is-collected]
To see exactly what your Panel sends, run:
```bash
php artisan p:telemetry
```
It includes:
* **Panel:**
* A random ID for your Panel installation.
* The Panel version and PHP version.
* The backup, cache, and database drivers, and the database version.
* **Totals:**
* Allocations and used allocations.
* Backups and their total size.
* Eggs, locations, and mounts.
* Nodes and servers, including suspended servers.
* Users, including administrators.
* **For each node:**
* Its ID and Wings version.
* Docker's version, cgroup driver and version, storage driver, and container counts.
* The system's architecture, CPU threads, memory, kernel version, and operating system.
The collection code is in `app/Services/Telemetry/TelemetryCollectionService.php`.
### Turning Telemetry Off or On [#turning-telemetry-off-or-on]
To turn telemetry off, set this in `.env`:
```bash
PTERODACTYL_TELEMETRY_ENABLED=false
```
To turn it back on, set it to `true` or remove the line. You can also choose during `php artisan p:environment:setup`, or pass it `--telemetry=false`.
# Docker Deployment
## Introduction [#introduction]
The Panel image runs the whole Panel in one container: NGINX, PHP 8.4, the queue worker, and the scheduler. You provide the database and Redis; Wings and game servers run on your nodes.
Before you start, read the [requirements](./requirements.mdx).
## Choosing an Image [#choosing-an-image]
Always deploy a specific version tag or digest. The example Compose file uses `ghcr.io/pterodactyl/panel:latest`, which does not point to 2.0 until 2.0 is released.
To build the image yourself, run this from a 2.0 source checkout:
```bash
docker build --pull -t pterodactyl-panel:2.0-local .
```
## Configuration [#configuration]
Copy `docker-compose.example.yml` to `compose.yaml`. If you built the image yourself, set the `panel` service's `image` to `pterodactyl-panel:2.0-local`. Review `APP_URL`, the database credentials, mail settings, ports, and mounted paths.
The example uses MariaDB 11.4 and Redis 7.4. These settings control the services inside the Panel container:
| Setting | Effect |
| --------------------------- | -------------------------------------------------------------------------- |
| `QUEUE_WORKER_ENABLED` | Set to `true` to run the queue worker. |
| `SCHEDULER_ENABLED` | Set to `true` to run the scheduler. Enable it on only one Panel container. |
| `CACHE_STORE=redis` | Stores the cache in Redis. |
| `SESSION_DRIVER=redis` | Stores sessions in Redis. Use it with `SESSION_CONNECTION=sessions`. |
| `QUEUE_CONNECTION=redis` | Stores queued jobs in Redis. |
| `APP_ENVIRONMENT_ONLY=true` | Reads Panel settings from the environment instead of the database. |
`QUEUE_WORKER_ENABLED` and `SCHEDULER_ENABLED` must be exactly `true` or `false`.
The container runs the queue worker and scheduler itself. Do not also set up the `pteroq` service or the cron entry from [Getting Started](./getting-started.mdx#queue-and-scheduler).
## Persistent Data [#persistent-data]
The example Compose file keeps these paths outside the container:
| Path | Contents |
| ------------------------------------------ | ------------------------------------------------------- |
| `/app/var` | The `.env` file, including `APP_KEY` and `HASHIDS_SALT` |
| `/etc/nginx/http.d` | NGINX configuration |
| `/etc/letsencrypt` | TLS certificates |
| `/app/storage/logs` | Panel logs |
| `/app/extensions` | Installed extensions |
| `/app/public/assets/extensions` | Extension assets |
| `/var/lib/mysql` in the database container | The Panel database |
| `/data` in the Redis container | Redis data |
On first start, the container creates `/app/var/.env` with new keys, and reuses it when you recreate the container.
**Back up the database and `/app/var/.env` together.** All Panel containers must share the same `APP_KEY` and `HASHIDS_SALT`. Never generate new keys for an existing Panel.
Game server files and their backups live on your Wings nodes, not in these volumes.
## Running Migrations [#running-migrations]
The container does not migrate the database on start. Run migrations yourself, once per deployment, before the new version serves traffic. That way, starting an extra container never changes your database.
For a new installation, start the database and Redis, migrate, create your first administrator, then start the Panel:
```bash
docker compose -f compose.yaml up -d database cache
docker compose -f compose.yaml run --rm panel php artisan migrate --seed --force --no-interaction
docker compose -f compose.yaml run --rm panel php artisan p:user:make
docker compose -f compose.yaml up -d panel
```
These steps are for new installations only. Moving an existing 1.x database into this image is not tested yet. To upgrade a native 1.x installation, see [Upgrading From 1.x](../upgrading/upgrading-from-v1.mdx).
## HTTPS [#https]
To get a Let's Encrypt certificate, set `APP_URL` to your `https://` address, set `LE_EMAIL`, and keep `/etc/letsencrypt` on a volume. Your domain must reach the container on port 80.
If a reverse proxy handles HTTPS, leave `LE_EMAIL` empty. Send the proxy's traffic to the container's HTTP port, and configure the Panel's trusted proxies.
## Wings [#wings]
The Compose file does not run Wings or give the Panel access to Docker. Install Wings separately on each node.
The Panel and Wings may share a machine. Game servers run in their own images, chosen by each egg, and those images must support your node's CPU architecture. An ARM64 Panel image does not mean every game supports ARM64.
# Getting Started
## Introduction [#introduction]
This guide installs the Panel on a Linux server where you have root access. You should be comfortable installing packages, editing files, and managing services.
The Panel's Docker image includes the web server, queue worker, and scheduler. See [Docker Deployment](./docker.mdx).
To upgrade a 1.x Panel, follow [Upgrading From 1.x](../upgrading/upgrading-from-v1.mdx).
## Choosing an Operating System [#choosing-an-operating-system]
Supported operating systems:
| Operating System | Versions | Notes |
| ---------------- | -------- | ----------------------------------------------------- |
| Ubuntu | 24.04 | Recommended. Includes PHP 8.3. |
| | 22.04 | Needs an extra repository for PHP 8.3. |
| Debian | 13 | Needs an extra repository for PHP. |
| | 12 | Needs extra repositories for PHP, MariaDB, and Redis. |
| | 11 | Needs extra repositories for PHP, MariaDB, and Redis. |
Pterodactyl officially supports Ubuntu, Debian, and the Panel's [Docker image](./docker.mdx). Other Linux distributions may work, but often need changes to these steps, and we cannot help with them.
Pick your operating system, PHP version, and web server. The commands on this page follow your choices.
## Installing Dependencies [#installing-dependencies]
The Panel needs:
* PHP 8.3 or 8.4, with the `cli`, `gd`, `mysql`, `mbstring`, `bcmath`, `xml`, `curl`, `zip`, and `intl` extensions. NGINX and Caddy also need PHP-FPM.
* MariaDB 10.11 or 11, or MySQL 8 or 9.
* Redis.
* A web server, such as NGINX, Apache, or Caddy.
* `curl`, `tar`, and `unzip`.
* Composer 2.
* Node.js 22.12 or newer, to build the frontend.
Ubuntu 24.04 includes PHP 8.3. Other PHP versions, and PHP on Ubuntu 22.04, need the PHP repository. Add it:
```bash
apt -y install software-properties-common
LC_ALL=C.UTF-8 add-apt-repository -y ppa:ondrej/php
```
Debian needs Sury's repository for PHP. Add it:
```bash
apt -y install curl ca-certificates gnupg2 lsb-release
echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" | tee /etc/apt/sources.list.d/sury-php.list
curl -fsSL https://packages.sury.org/php/apt.gpg | gpg --dearmor -o /etc/apt/trusted.gpg.d/sury-keyring.gpg
```
Debian 11 and 12 also need the Redis and MariaDB repositories:
```bash
curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/redis.list
curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | bash
```
Install everything:
```bash
apt update
apt -y install php8.3 php8.3-{common,cli,gd,mysql,mbstring,bcmath,xml,fpm,curl,zip,intl} mariadb-server redis-server tar unzip curl nginx
```
Install everything, including Apache's PHP module:
```bash
apt update
apt -y install php8.3 php8.3-{common,cli,gd,mysql,mbstring,bcmath,xml,curl,zip,intl} mariadb-server redis-server tar unzip curl apache2 libapache2-mod-php8.3
```
Caddy is not in Ubuntu's default repositories. Add Caddy's repository:
```bash
apt -y install debian-keyring debian-archive-keyring apt-transport-https curl gnupg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
```
Then install everything:
```bash
apt update
apt -y install php8.3 php8.3-{common,cli,gd,mysql,mbstring,bcmath,xml,fpm,curl,zip,intl} mariadb-server redis-server tar unzip curl caddy
```
### Installing Composer [#installing-composer]
Install Composer, the PHP dependency manager:
```bash
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
```
### Installing Node.js [#installing-nodejs]
Ubuntu's Node.js is too old to build the frontend. Install Node.js 22 from NodeSource:
```bash
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt -y install nodejs
```
## Downloading the Panel [#downloading-the-panel]
Create the Panel's directory and move into it:
```bash
mkdir -p /var/www/pterodactyl
cd /var/www/pterodactyl
```
2.0 has no release yet, so download the `2.0-develop` branch. Unpack it and make `storage` and `bootstrap/cache` writable:
```bash
curl -Lo panel.zip https://github.com/pterodactyl/panel/archive/refs/heads/2.0-develop.zip
unzip -q panel.zip
cp -a panel-2.0-develop/. .
rm -rf panel-2.0-develop panel.zip
chmod -R 755 storage/* bootstrap/cache/
```
The branch has no built frontend. Build it:
```bash
npm ci
npm run build:production
```
## Creating the Database [#creating-the-database]
The Panel needs its own database and a user with access to it. Open the MariaDB console:
```bash
mariadb -u root
```
Create the user and database. Replace `yourPassword` with a strong, unique password:
```sql
CREATE USER 'pterodactyl'@'127.0.0.1' IDENTIFIED BY 'yourPassword';
CREATE DATABASE panel;
GRANT ALL PRIVILEGES ON panel.* TO 'pterodactyl'@'127.0.0.1' WITH GRANT OPTION;
exit
```
## Installation [#installation]
### Installing PHP Dependencies [#installing-php-dependencies]
Copy the example environment file, install PHP dependencies, and generate the application key:
```bash
cp .env.example .env
COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader
php artisan key:generate --force
```
Only run `php artisan key:generate` on a new installation. On an existing Panel, it makes encrypted data unreadable.
`APP_KEY` in `.env` encrypts sensitive data, such as API keys, node tokens, and two-factor secrets. **If you lose it, that data cannot be recovered, even from a database backup.**
Save a copy off the server, such as in a password manager. To print it, run:
```bash
grep APP_KEY /var/www/pterodactyl/.env
```
### Configuring the Environment [#configuring-the-environment]
These commands set up the environment. They ask about your Panel's address, sessions, caching, the database, and email:
```bash
php artisan p:environment:setup
php artisan p:environment:database
php artisan p:environment:mail
```
In `p:environment:setup`, choose Redis for the cache, session, and queue drivers. For email, choose `smtp` to use your own mail server. The `mail` option uses PHP's built-in mail sending, which is not recommended.
### Setting Up the Database [#setting-up-the-database]
Create the database tables and add the default eggs:
```bash
php artisan migrate --seed --force
```
This may take a while. Do not stop it before it finishes.
### Creating the First User [#creating-the-first-user]
Create an administrator account to sign in with:
```bash
php artisan p:user:make
```
### Setting Permissions [#setting-permissions]
Give the web server user ownership of the Panel's files:
```bash
chown -R www-data:www-data /var/www/pterodactyl/*
```
## Queue and Scheduler [#queue-and-scheduler]
The Panel sends email, runs server schedules, and does other background work through a scheduler and a queue worker.
### Scheduler [#scheduler]
Open root's crontab:
```bash
crontab -e
```
Add this line to run the scheduler every minute:
```text
* * * * * php /var/www/pterodactyl/artisan schedule:run >> /dev/null 2>&1
```
### Queue Worker [#queue-worker]
Create `pteroq.service` in `/etc/systemd/system`:
```text
[Unit]
Description=Pterodactyl Queue Worker
After=redis-server.service
[Service]
User=www-data
Group=www-data
Restart=always
ExecStart=/usr/bin/php /var/www/pterodactyl/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3
StartLimitInterval=180
StartLimitBurst=30
RestartSec=5s
[Install]
WantedBy=multi-user.target
```
Enable Redis and the queue worker so they start on boot:
```bash
systemctl enable --now redis-server
systemctl enable --now pteroq.service
```
## Next Steps [#next-steps]
Next, [configure your web server](./webserver-configuration.mdx) to open the Panel in your browser. Then [install Wings](../wings/installing.mdx) on each node.
### Telemetry [#telemetry]
The Panel sends anonymous usage statistics to help improve Pterodactyl. `p:environment:setup` asks whether to enable it. See [Telemetry](./additional-configuration.mdx#telemetry) for what is collected and how to turn it off.
# Requirements
## Introduction [#introduction]
The Panel needs the software below. The [Docker image](./docker.mdx) includes everything except the database and Redis.
## Panel Requirements [#panel-requirements]
Your server needs:
* PHP 8.3 or 8.4
* Composer 2
* MariaDB 10.11 or 11, or MySQL 8 or 9
* Redis
* A web server, such as NGINX, that serves the Panel's `public` directory
* A queue worker and a cron entry for the scheduler
The Panel is tested against each of these PHP and database versions. Other databases, such as PostgreSQL and SQLite, are not supported.
### PHP Extensions [#php-extensions]
Most PHP installations include some required extensions, such as OpenSSL, Sodium, and Tokenizer. These usually need a separate install:
* BCMath
* Intl
* Mbstring
* PDO MySQL
* XML
* Zip
On Ubuntu and Debian, install PHP 8.3 with every required extension. Debian and Ubuntu 22.04 first need the PHP repository from [Getting Started](./getting-started.mdx#installing-dependencies):
```bash
apt -y install php8.3 php8.3-{common,cli,gd,mysql,mbstring,bcmath,xml,fpm,curl,zip,intl}
```
To check that PHP has everything, run this from the Panel directory:
```bash
composer check-platform-reqs --no-dev
```
If PHP-FPM uses a different PHP installation than the command line, give it the same extensions.
### Building From Source [#building-from-source]
2.0 has no release yet, so a Panel installed from the `2.0-develop` branch must build its frontend. The Docker image includes a built frontend.
This needs Node.js 22.12 or newer. From the Panel directory, run:
```bash
npm ci
npm run build:production
```
## Operating Systems [#operating-systems]
Pterodactyl officially supports two ways to run the Panel:
* A native installation on Ubuntu 22.04 or 24.04, or Debian 11, 12, or 13. See [Getting Started](./getting-started.mdx).
* The [Docker image](./docker.mdx), which supports AMD64 and ARM64.
Other Linux distributions may work, but often need changed installation steps, and we cannot help with them. The upgrade from 1.x was tested on Ubuntu 24.04.
## Wings [#wings]
Wings runs on each node and manages your game servers with Docker. Install it separately. It is not part of the Panel image.
There is no Wings version table for 2.0 yet. Wings 1.13.2 and 1.13.3 work with the 2.0 Panel, and handled all of this in testing without changes:
* Power actions and server status.
* Files and the console.
* SFTP, with passwords and with SSH keys.
* Schedules.
* Backups.
{/* Verified against panel-next 18a0cf2c: composer.json, Dockerfile, package.json, and the CI test matrix (PHP 8.3/8.4, mariadb:10/11, mysql:8/9). */}
# Troubleshooting
## Matching an Error [#matching-an-error]
Paste an error message or log line below. If it matches a known problem, you see the cause and the fix. Every problem it knows is also covered on this page or the page it links to.
## Reading Error Logs [#reading-error-logs]
When the Panel shows an unexpected error, check its log first. To show the last 100 lines of today's log, run:
```bash
tail -n 100 /var/www/pterodactyl/storage/logs/laravel-$(date +%F).log
```
For Wings, read its service log on the node:
```bash
journalctl -u wings -n 100 --no-pager
```
To collect the details support usually asks for, such as versions, main configuration values, and recent log lines, run `wings diagnostics` on the node. It asks whether to include your Panel's address and the logs, and whether to review the report before upload. Answer yes to the review so you can check it first.
### Finding the Error [#finding-the-error]
Each error in the Panel's log starts with a line that has the date and time in brackets, followed by a long stack trace. The dated line describes the problem, for example:
```text
[2026-09-29 10:15:02] production.ERROR: file_put_contents(/var/www/pterodactyl/storage/framework/views/...): Failed to open stream: Permission denied
```
Here the web server cannot write to `storage`, which usually means wrong file permissions. You can ignore the stack trace below the line.
To show only these lines, without stack traces, run:
```bash
tail -n 1000 /var/www/pterodactyl/storage/logs/laravel-$(date +%F).log | grep "\[$(date +%Y)"
```
Include these lines when you ask for help. Remove any passwords or keys first.
## The Panel Shows a Blank Page [#the-panel-shows-a-blank-page]
If the Panel loads a blank page, open your browser's developer console to see the error.
An extension can stop pages from loading. To check, set `PTERODACTYL_EXTENSIONS_ENABLED=false` in `.env`, run `php artisan config:clear`, and reload. If the Panel works again, find the faulty extension with `php artisan p:extension:list` and disable it with `php artisan p:extension:disable`. See [Extensions](../extensions/index.mdx#troubleshooting).
If the log says `The Vite manifest has not been generated yet`, the frontend has not been built. See [Building From Source](./requirements.mdx#building-from-source).
## 502 Bad Gateway [#502-bad-gateway]
NGINX and Caddy pass PHP requests to PHP-FPM through a socket, such as `/run/php/php8.3-fpm.sock`. A `502 Bad Gateway` means that socket does not exist: PHP-FPM is stopped, or your web server configuration names a PHP version that is not installed. This often follows a PHP upgrade.
```bash
systemctl status php8.3-fpm
ls /run/php/
```
Make sure your web server configuration names a socket that `ls` shows, then reload the web server. See [Updating the Web Server](../guides/configuration/php-upgrade.mdx#updating-the-web-server).
## Sign-In Fails With a CSRF or 419 Error [#sign-in-fails-with-a-csrf-or-419-error]
If signing in fails with `CSRF token mismatch.` or `419 Page Expired`, your browser is not sending the session cookie back. Usually `APP_URL` in `.env` does not match the address you open the Panel at:
* If `APP_URL` starts with `https://`, `php artisan p:environment:setup` sets `SESSION_SECURE_COOKIE=true`. Browsers then send the cookie only over HTTPS, so signing in over plain HTTP always fails.
* The Panel treats only requests from the host in `APP_URL` as signed-in browser requests. A different domain or IP address does not work.
Set `APP_URL` to the exact address you use, then clear the configuration cache:
```bash
php artisan config:clear
```
Behind a reverse proxy or Cloudflare, also set `TRUSTED_PROXIES`, as described in [Reverse Proxies](./additional-configuration.mdx#reverse-proxies).
## Cannot Connect to a Server [#cannot-connect-to-a-server]
### Basic Checks [#basic-checks]
* **Wings is running.** Check it with `systemctl status wings`.
* **The browser shows no errors.** Open the developer console and look for red errors.
* **The node's configuration matches.** Wings' `/etc/pterodactyl/config.yml` must match the node's **Configuration** tab under **Admin → Nodes**.
* **The firewall allows Wings' ports.** Wings uses `8080` (or the node's **Daemon Port**) for its API, and `2022` for SFTP.
* **No ad blocker is blocking the Panel or Wings.**
* **The Panel can reach Wings.** On the Panel's server, run `curl https://node.example.com:8080` with your node's domain.
* **The Panel and Wings use the same scheme.** If the Panel uses HTTPS, Wings must too.
* **Certificates are valid.** If Wings uses HTTPS, check that its certificate has not expired.
`Could not establish a connection to the machine running this server` means Wings did not answer at all. `There was an error while communicating with the machine running this server` means Wings answered with an error; find the message's `request_id` in the Wings log.
### Advanced Checks [#advanced-checks]
* **Run Wings in debug mode.** Stop Wings and run `wings --debug` to see its errors. For help, ask on [Discord](https://discord.gg/pterodactyl).
* **Check DNS.** Use `dig` or `nslookup` to confirm your domains point to the right addresses.
* **Check Cloudflare.** Turn off Cloudflare's proxy (the orange cloud) for your Wings domain.
* **Check NAT.** If Wings is behind a firewall, such as pfSense, forward the correct ports to it.
* **Add host entries.** When the Panel and Wings share a server, an `/etc/hosts` entry pointing the Panel's domain at the server can help.
## The Console Does Not Connect [#the-console-does-not-connect]
The console does not go through the Panel. Your browser connects straight to Wings, at the node's FQDN and Daemon Port. If the console stays disconnected or shows "Server connection failed", check:
* **The scheme.** If the Panel uses HTTPS, the node must use SSL too. Browsers block insecure connections from a secure page.
* **The certificate.** Open `https://:8080` in your browser. Any certificate warning there also breaks the console.
* **The origin.** Wings only accepts console connections from pages at the `remote` address in its `config.yml`. It must match your browser's address bar exactly, including `https://`, with no trailing slash. List any other Panel addresses under `allowed_origins`. See [Panel Connection](../wings/configuration.mdx#panel-connection).
"Server connection rejected" means Wings and the Panel disagree on the node's token. Run the auto-deploy command from the node's **Configuration** tab again with `--override`, and restart Wings.
## SFTP Login Fails [#sftp-login-fails]
* **Use the right username:** your Panel username, a period, and the server's 8-character ID, such as `admin.1a2b3c4d`. Copy it from **SFTP Details** on the server's **Settings** page.
* **Use the right port.** Wings' SFTP server listens on `2022` by default, not `22`.
* **Use your Panel password**, or an SSH key you added to your account.
* **Subusers need the SFTP permission.** The server's owner and administrators always have it.
* **Wait after repeated failures.** Wings and the Panel rate-limit login attempts.
On the node, `journalctl -u wings` shows why Wings refused a login, for example `failed to validate user credentials (invalid format)` for a badly formatted username.
## Wings Does Not Start [#wings-does-not-start]
Run `journalctl -u wings -n 50 --no-pager`, or stop the service and run `wings --debug`. Look for the last error before Wings exits:
* **`Configuration File Not Found`:** `/etc/pterodactyl/config.yml` does not exist. See [Configuring Wings](../wings/installing.mdx#configuring-wings).
* **`failed to configure HTTPS server`:** SSL is on, but the certificate or key file is missing. See [Creating SSL Certificates](../guides/tutorials/ssl-certificates.mdx).
* **`address already in use`:** another program uses Wings' API or SFTP port.
* **`failed to configure docker environment`:** Docker is not running. Start it with `systemctl enable --now docker`.
* **`failed to load server configurations`:** Wings could not get its servers from the Panel. If the error ends in `(HTTP/403)`, the node's token does not match; run the auto-deploy command again with `--override`. Otherwise, check that `remote` is reachable from the node.
## Invalid MAC Exception [#invalid-mac-exception]
This error only happens when `APP_KEY` does not match the key that encrypted the data, usually because a database backup was restored into a new installation without the original `.env` file. **Always restore the `.env` file together with the database.**
The log shows it as an invalid MAC when decrypting. The only fix is to restore the original `APP_KEY` in `.env`. If that key is lost, the encrypted data cannot be recovered.
## Servers Have No Internet Access [#servers-have-no-internet-access]
This is usually DNS. By default, Wings gives containers the DNS servers `1.1.1.1` and `1.0.0.1`, which some hosts block.
To find your host's DNS servers, try:
```bash
# NetworkManager
nmcli -g ip4.dns,ip6.dns dev show
# systemd-resolved (recent Ubuntu versions)
resolvectl status
```
Or look in `/etc/resolv.conf` or `/etc/network/interfaces`.
Replace `1.1.1.1` and `1.0.0.1` under `docker.network.dns` in Wings' `/etc/pterodactyl/config.yml` with your host's servers. Restart Wings, then restart the server so its container uses the new servers.
## Schedules Do Not Run [#schedules-do-not-run]
* **Check the queue worker's log** with `journalctl -xeu pteroq`, and restart it with `systemctl restart pteroq`.
* **Clear the scheduler's cache** with `php /var/www/pterodactyl/artisan schedule:clear-cache`.
* **Check the cron entry.** Run `crontab -l` as root and confirm the scheduler line from [Getting Started](./getting-started.mdx#scheduler) is there.
* **Check PHP.** Compare `php -v` with the [requirements](./requirements.mdx).
* **Test the schedule on its own.** Make its first task something visible in the console, such as `say test` on a Minecraft server, to tell whether the schedule or its tasks are the problem.
* **Check the time zones.** Schedules at the wrong time usually mean mismatched time zones. Compare the system's (`timedatectl`), the Panel's (`APP_TIMEZONE` in `.env`), and Wings' (`timezone` in `config.yml`).
* **Check the database and Redis.** Run `systemctl status mariadb` and `systemctl status redis-server`. If either is stopped, check its log with `journalctl -xeu`.
* **Check the Panel's log**, as described in [Reading Error Logs](#reading-error-logs).
# Updating the Panel
## Introduction [#introduction]
This guide updates the Panel within 2.x, such as from 2.0.0 to 2.0.1.
These steps do not upgrade a 1.x Panel. To move from 1.x to 2.0, follow [Upgrading From 1.x](../upgrading/upgrading-from-v1.mdx).
Before you update, check the release notes for new requirements and make sure you meet the [requirements](./requirements.mdx). Always keep a recent backup of your database and `.env` file.
## Update Steps [#update-steps]
### Enter Maintenance Mode [#enter-maintenance-mode]
Enter maintenance mode so users do not see errors during the update:
```bash
cd /var/www/pterodactyl
php artisan down
```
### Download the Update [#download-the-update]
2.0 has no release yet, so download the latest `2.0-develop` branch and unpack it over the current files:
```bash
curl -Lo panel.zip https://github.com/pterodactyl/panel/archive/refs/heads/2.0-develop.zip
unzip -q panel.zip
cp -a panel-2.0-develop/. .
rm -rf panel-2.0-develop panel.zip
chmod -R 755 storage/* bootstrap/cache
```
### Update Dependencies [#update-dependencies]
Install PHP dependencies and rebuild the frontend:
```bash
COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader
npm ci
npm run build:production
```
### Clear Cached Files [#clear-cached-files]
Clear compiled views and cached configuration:
```bash
php artisan view:clear
php artisan config:clear
```
### Update the Database [#update-the-database]
Update the database and the default eggs:
```bash
php artisan migrate --seed --force
```
The seeder replaces the default eggs with their latest versions. Do not edit the default eggs; copy them instead, or your changes are overwritten.
### Set Permissions [#set-permissions]
Give the web server user ownership of the files again:
```bash
chown -R www-data:www-data /var/www/pterodactyl/*
```
### Restart the Queue Worker [#restart-the-queue-worker]
Restart the queue worker so it runs the new code:
```bash
php artisan queue:restart
```
### Exit Maintenance Mode [#exit-maintenance-mode]
Bring the Panel back online:
```bash
php artisan up
```
## Updating With One Command [#updating-with-one-command]
`p:upgrade` only installs releases, and 2.0 has none yet. Without `--release` it downloads the latest release, which is 1.x, and breaks a 2.0 Panel. Use the manual steps above.
`p:upgrade` runs all of the steps above. Pass the release to install:
```bash
php artisan p:upgrade --release=
```
Without `--release`, it downloads the latest release. It asks you to confirm the web server's user and group before it starts; pass `--user` and `--group` to skip the questions.
# Webserver Configuration
## Introduction [#introduction]
The web server passes requests to PHP and serves `/var/www/pterodactyl/public`. Pick your web server and enter your Panel's domain or IP address; the configuration below fills them in.
SSL configurations need a certificate for your domain first, or the web server will not start. See [Creating SSL Certificates](../guides/tutorials/ssl-certificates.mdx). Caddy with automatic SSL creates its own.
Remove the default NGINX site:
```bash
rm /etc/nginx/sites-enabled/default
```
Create `/etc/nginx/sites-available/pterodactyl.conf`:
```nginx title="pterodactyl.conf"
server {
listen 80;
listen [::]:80;
server_name ;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ;
root /var/www/pterodactyl/public;
index index.php;
access_log /var/log/nginx/pterodactyl.app-access.log;
error_log /var/log/nginx/pterodactyl.app-error.log error;
# Allow larger uploads and longer requests.
client_max_body_size 100m;
client_body_timeout 120s;
sendfile off;
ssl_certificate /etc/letsencrypt/live//fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live//privkey.pem;
ssl_session_cache shared:SSL:10m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
ssl_prefer_server_ciphers on;
# See https://hstspreload.org/ before you uncomment the next line.
# add_header Strict-Transport-Security "max-age=15768000; preload;";
add_header X-Content-Type-Options nosniff;
add_header X-Robots-Tag none;
add_header Content-Security-Policy "frame-ancestors 'self'";
add_header X-Frame-Options DENY;
add_header Referrer-Policy same-origin;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ^~ /assets/ {
location ~ /\. {
deny all;
}
try_files $uri =404;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size=100M";
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_PROXY "";
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
}
location ~ /\.ht {
deny all;
}
}
```
Enable the site and restart NGINX:
```bash
ln -s /etc/nginx/sites-available/pterodactyl.conf /etc/nginx/sites-enabled/pterodactyl.conf
systemctl restart nginx
```
Remove the default NGINX site:
```bash
rm /etc/nginx/sites-enabled/default
```
Create `/etc/nginx/sites-available/pterodactyl.conf`:
```nginx title="pterodactyl.conf"
server {
listen 80;
listen [::]:80;
server_name ;
root /var/www/pterodactyl/public;
index index.html index.htm index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ^~ /assets/ {
location ~ /\. {
deny all;
}
try_files $uri =404;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log /var/log/nginx/pterodactyl.app-error.log error;
# Allow larger uploads and longer requests.
client_max_body_size 100m;
client_body_timeout 120s;
sendfile off;
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size=100M";
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_PROXY "";
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
}
location ~ /\.ht {
deny all;
}
}
```
Enable the site and restart NGINX:
```bash
ln -s /etc/nginx/sites-available/pterodactyl.conf /etc/nginx/sites-enabled/pterodactyl.conf
systemctl restart nginx
```
Install Apache and its PHP module, then disable the default site:
```bash
apt -y install apache2 libapache2-mod-php8.3
a2dissite 000-default.conf
```
Create `/etc/apache2/sites-available/pterodactyl.conf`:
```apache title="pterodactyl.conf"
ServerName
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L]
ServerName
DocumentRoot "/var/www/pterodactyl/public"
AllowEncodedSlashes On
php_value upload_max_filesize 100M
php_value post_max_size 100M
Require all granted
AllowOverride all
AllowOverride None
php_admin_flag engine off
Require all denied
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live//fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live//privkey.pem
```
Enable the site and its modules, and restart Apache:
```bash
a2ensite pterodactyl.conf
a2enmod rewrite ssl
systemctl restart apache2
```
Install Apache and its PHP module, then disable the default site:
```bash
apt -y install apache2 libapache2-mod-php8.3
a2dissite 000-default.conf
```
Create `/etc/apache2/sites-available/pterodactyl.conf`:
```apache title="pterodactyl.conf"
ServerName
DocumentRoot "/var/www/pterodactyl/public"
AllowEncodedSlashes On
php_value upload_max_filesize 100M
php_value post_max_size 100M
AllowOverride all
Require all granted
AllowOverride None
php_admin_flag engine off
Require all denied
```
Enable the site and the rewrite module, and restart Apache:
```bash
a2ensite pterodactyl.conf
a2enmod rewrite
systemctl restart apache2
```
Caddy gets and renews a certificate automatically. Your domain must point at the server, and ports 80 and 443 must be reachable from the internet.
Replace the contents of `/etc/caddy/Caddyfile` with:
```text title="Caddyfile"
{
servers :443 {
timeouts {
read_body 120s
}
}
}
{
root * /var/www/pterodactyl/public
file_server
@assets-dotfiles path_regexp ^/assets/(.*/)?\.
respond @assets-dotfiles 403
@php not path /assets/*
php_fastcgi @php unix//run/php/php8.3-fpm.sock {
root /var/www/pterodactyl/public
index index.php
env PHP_VALUE "upload_max_filesize = 100M
post_max_size = 100M"
env HTTP_PROXY ""
env HTTPS "on"
read_timeout 300s
dial_timeout 300s
write_timeout 300s
}
header Strict-Transport-Security "max-age=16768000; preload;"
header X-Content-Type-Options "nosniff"
header X-Robots-Tag "none"
header Content-Security-Policy "frame-ancestors 'self'"
header X-Frame-Options "DENY"
header Referrer-Policy "same-origin"
request_body {
max_size 100m
}
respond /.ht* 403
log {
output file /var/log/caddy/pterodactyl.log {
roll_size 100MiB
roll_keep_for 7d
}
level INFO
}
}
```
Behind Cloudflare's proxy, Caddy needs a DNS challenge to get its certificate. See [Caddy's documentation](https://caddyserver.com/docs/automatic-https#dns-challenge).
Restart Caddy:
```bash
systemctl restart caddy
```
Replace the contents of `/etc/caddy/Caddyfile` with:
```text title="Caddyfile"
{
servers :80 {
timeouts {
read_body 120s
}
}
}
:80 {
root * /var/www/pterodactyl/public
file_server
@assets-dotfiles path_regexp ^/assets/(.*/)?\.
respond @assets-dotfiles 403
@php not path /assets/*
php_fastcgi @php unix//run/php/php8.3-fpm.sock {
root /var/www/pterodactyl/public
index index.php
env PHP_VALUE "upload_max_filesize = 100M
post_max_size = 100M"
env HTTP_PROXY ""
read_timeout 300s
dial_timeout 300s
write_timeout 300s
}
header X-Content-Type-Options "nosniff"
header X-Robots-Tag "none"
header Content-Security-Policy "frame-ancestors 'self'"
header X-Frame-Options "DENY"
header Referrer-Policy "same-origin"
request_body {
max_size 100m
}
respond /.ht* 403
log {
output file /var/log/caddy/pterodactyl.log {
roll_size 100MiB
roll_keep_for 7d
}
level INFO
}
}
```
Restart Caddy:
```bash
systemctl restart caddy
```
## After Configuring Your Web Server [#after-configuring-your-web-server]
Open your Panel's address and sign in with the user you created. If you switched from HTTP to HTTPS, make sure `APP_URL` in `.env` starts with `https://`, then run `php artisan config:clear`.
# About
## Core Project Team [#core-project-team]
| Name | GitHub | Primary Role |
| ------------- | ---------------------------------------------------------- | ------------------ |
| Robert Dennis | [@robertdrakedennis](https://github.com/robertdrakedennis) | Project Maintainer |
| Sky Mulley | [@SkyMulley](https://github.com/SkyMulley) | Project Maintainer |
Members of the project team have a red username in our Discord server.
## Community Team [#community-team]
Pterodactyl owes much of its success to our community support team. Its members have a yellow username in our Discord server.
## Sponsors [#sponsors]
These companies help fund Pterodactyl's development. [Interested in becoming a sponsor?](https://github.com/sponsors/pterodactyl)
| Company | About |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [**Buildurly**](https://buildurly.com/) | Buildurly is a hardware procurement company. They deliver tailored, enterprise-grade hardware solutions designed around your unique needs. From sourcing to delivery, Buildurly's white-glove service ensures a seamless, worry-free, professional experience. |
| [**BuiltByBit**](https://builtbybit.com/) | BuiltByBit is a marketplace for game server and community assets. Creators sell plugins, setups, builds, bots and websites for Minecraft, Roblox, Hytale, Garry's Mod and Discord. |
| [**Hosturly**](https://hosturly.com/) | Hosturly is an enterprise hosting provider. They provide cost-effective, high-performance, and reliable services, including VPS, Web, Dedicated, and Colocation. |
| [**indifferent broccoli**](https://indifferentbroccoli.com/) | indifferent broccoli is a game server hosting and rental company. With them, you get top-notch computer power for your gaming sessions. They destroy lag, latency, and complexity--letting you focus on the fun stuff. |
| [**Infraly, LLC**](https://infraly.co/) | Infraly is an infrastructure company powering the next generation of online services. Through their brands, Infraly delivers cutting-edge solutions across multiple markets. Their vertically integrated approach provides unmatched performance, scalability, and reliability, giving our customers full control. |
| [**MineStrator**](https://minestrator.com/) | MineStrator is a game server hosting provider. Looking for the most high-end French hosting company for your Minecraft server? More than 24,000 members on our Discord trust us. Give us a try! |
| [**Physgun**](https://physgun.com/) | Physgun is a game server hosting provider. Most providers rent rack space and rebrand a panel. At Physgun, they engineer the performance, write the features, and staff the support. Physgun truly is game hosting perfected! |
| [**WISP**](https://wisp.gg/) | WISP is an industry-leading SaaS platform for game server management, designed for hosting companies, gaming organizations, and enthusiasts. WISP combines modern, intuitive interfaces with powerful tools, making server deployment and administration seamless, scalable, and efficient. |
## License [#license]
Copyright © 2015 Pterodactyl®.
Code released under the [MIT License](https://github.com/pterodactyl/panel/blob/develop/LICENSE.md).
# Community Standards
Pterodactyl has grown from a community of tens in 2015 to a community of thousands in 2020. During that time
there have been countless growing pains and community has changed in an innumerable number of ways. At our heart
however, Pterodactyl continues to exist for one purpose: to be *the* platform for running your game servers.
In order to keep true to that goal, and continue to foster one of the largest open-source game panel communities
out there, we've adopted a simple set of guidelines for participating in this community. The goal of these guidelines
is to foster an inclusive, welcoming environment for new users, and provide a space for the thousands of existing
users, administrators, network owners, and hosting companies to co-exist.
These rules and guidelines extend to all facets of the Pterodactyl Community, including but not limited to our
Discord Server and all activities within the GitHub Organization.
## Community Guidelines [#community-guidelines]
At the most basic level, these guidelines can be distilled down to:
1. Be a decent human.
2. Patience is a virtue.
### Be Mature [#be-mature]
You are expected to be mature and control your behavior in a manner that adheres to basic human decency. If you are
unable to do this you will be removed from the community. Personal attacks, spam (in any form), "doxxing", or otherwise
acting out is not allowed.
This community is fairly lax in regards to moderating language. However, the following are some examples of
behavior that is absolutely *not* tolerated and for which you will be removed from the community.
* Racist, sexist, homophobic, transphobic, or otherwise derogatory speech, images, insinuations, or any language whose
sole purpose is to denigrate any individual, organization, or class of individual.
* Threats of violence against any person, group, or organization including "doxxing" of these entities.
* Pornographic or excessively violent content.
### Limit the Drama [#limit-the-drama]
Discussion, including linking to or discussing sites or software, that exists to cast a negative image of other
companies or users is not allowed. This includes calling out hosts using nulled software, attempting to elicit negative
reactions towards services or websites, or otherwise stirring up drama.
Assume someone is acting in good faith when responding to them. You don't have to agree with everyone, and you
don't need to respond to everything.
### Be Patient [#be-patient]
This is an open-source project. No members of the development team are paid in an official capacity to write,
maintain, nor support this software. The following actions are discouraged in this community.
* Repeatedly asking identical questions within the same channel (or across channels) within short periods of time.
* It is expected that some questions will be missed. If it has been a reasonable amount of time and your question
remains unanswered, you're welcome to re-post it.
* Keep all support questions within the realm of the support channels.
* Do not interrupt conversations in non-support channels solely to request that someone look in a support channel
and help you.
### No Commercial Services [#no-commercial-services]
Discussion of paid installation/upgrade services, modifications, or any other commercial offerings is strictly
prohibited unless otherwise noted. This also includes reaching out to individuals via Direct Message and offering
your services without provocation.
Advertising commercial services within your username or display name on Discord is forbidden.
[Sponsors](./about.mdx#sponsors) at the silver tier and higher are exempt from this rule.
### No Mention or Ping Spam [#no-mention-or-ping-spam]
Please, do not direct message any administrative, development, or notable community members without first
checking with them. Keep all support queries within the public support channels unless you have been directly
asked to move it elsewhere.
*But what if I am trying to respond back to someone?* That is fine! We only ask that you not mention people
directly if they're not already involved in a discussion with you.
# How Pterodactyl Works
Pterodactyl has two parts. The **Panel** is the website where you manage users, nodes, and servers. **Wings** runs on each node and runs your game servers in Docker containers.
## Terms [#terms]
* **Panel.** The website where you manage users, nodes, and servers. It runs on a web server (NGINX, Apache, or Caddy) with PHP, keeps its data in MariaDB or MySQL, and uses Redis for sessions, caching, and background jobs.
* **Node.** A machine that runs Wings. One node can host many servers. The Panel and Wings can run on the same machine.
* **Wings.** The Go service on each node. It runs servers in Docker, streams their consoles to your browser, and serves their files over SFTP.
* **Server.** A game or application server created in the Panel. Each server runs in its own container on a node, and players connect to it directly on the ports you assign to it.
* **Container.** The isolated environment Docker creates for each server. It limits the server's CPU, memory, and disk use, and keeps it apart from other servers on the same node.
* **Docker image.** A package with everything a server needs to run, such as Java for a Minecraft server. Pterodactyl publishes its images as [yolks](../guides/egg-creation/egg-docker-images.mdx).
* **Egg.** The configuration for one type of server, such as Paper or BungeeCord for Minecraft: its Docker image, startup command, settings, and install script.
* **Tag.** A label on an egg or a node. Tags group eggs, for example by game, and decide which nodes a server can be deployed to. Tags replace the nests used in 1.x.
* **Extension.** A package that adds features to the Panel. See [Extensions](../extensions/index.mdx).
## Ports [#ports]
| From | To | Default port | Used for |
| ----------- | ------------ | -------------------- | ------------------------------------------------------ |
| Browser | Panel | 443 (80 without SSL) | The Panel website |
| Browser | Wings | 8080 | Consoles, file uploads and downloads |
| SFTP client | Wings | 2022 | Server files |
| Panel | Wings | 8080 | Creating servers, managing files, installs and backups |
| Wings | Panel | 443 (80 without SSL) | Server settings, results, and SFTP logins |
| Players | Game servers | The server's ports | Playing the game |
You can change 8080 and 2022 for each node in the Panel.
## What This Means for Your Setup [#what-this-means-for-your-setup]
* The Panel must reach each node on port 8080, and each node must reach the Panel.
* Your users' browsers connect to nodes directly, so each node needs a domain name they can reach. If the Panel uses HTTPS, each node needs its own SSL certificate.
* Game servers keep running when the Panel is down, because Wings runs them.
To set up a node, see [Installing Wings](../wings/installing.mdx).
# Introduction
Pterodactyl is an open-source game server management panel built with PHP, React, and Go. It runs every game server in its own isolated Docker container, and gives administrators and users a fast, friendly interface to manage them.
## Supported Games [#supported-games]
Because each game runs in its own Docker container, you can host many different games on one machine without installing their dependencies on it.
The Panel ships with eggs for these games:
* Minecraft, including Vanilla, Paper, Forge, Sponge, and BungeeCord
* Rust
* Counter-Strike: Global Offensive
* Team Fortress 2
* Garry's Mod
* ARK: Survival Evolved
* Insurgency
* Teamspeak 3
* Mumble
The community maintains eggs for many more games and services, such as Terraria, Factorio, and FiveM, at [eggs.pterodactyl.io](https://eggs.pterodactyl.io). You can import them from **Admin → Eggs**.
## Responsible Disclosure [#responsible-disclosure]
If you find a security issue, email `support@pterodactyl.io`. Do not report it on our public bug tracker.
# Using AI With Pterodactyl
## Introduction [#introduction]
An AI assistant is a tool, like a search engine or a forum post. Pterodactyl does not build, test, or support any assistant, and this documentation is written for people. If you use one, you run the commands, so the results are yours.
## You Are Responsible [#you-are-responsible]
Pterodactyl, its maintainers, and its community are not responsible for anything an assistant tells you, or for what happens when you follow it, including lost data, a broken Panel, or a compromised server.
Assistants make things up. They mix 1.x and 2.0, invent commands and settings, and sound just as confident when they are wrong. Before you use anything an assistant gives you:
* **Understand it.** If you cannot explain what a command does, do not run it.
* **Check it against the docs.** The documentation is the reference: when an assistant and a page disagree, the page wins.
* **Try it on a copy first.** Back up your database and Panel directory before any upgrade or change.
* **Keep your secrets out of it.** Never paste your `.env` file, `APP_KEY`, API keys, node tokens, or database passwords into a chat.
## Getting Help [#getting-help]
The community helps people run Pterodactyl. It does not review, fix, or finish an assistant's work, and nobody owes you a walkthrough of its mistakes.
If you get stuck after using an assistant, go back to the documentation and follow it yourself. If you still need help:
* Say that you used an assistant, and what it had you change.
* Share the exact commands you ran, the full error, and the relevant logs, not the assistant's explanation of them.
* Accept that helpers may decline. They volunteer their time and decide how to spend it.
Do not use an assistant to write bug reports, pull requests, or messages to the community. The [contributing guide](https://github.com/pterodactyl/panel/blob/2.0-develop/CONTRIBUTING.md) requires you to disclose any AI help in a pull request, and to write titles, descriptions, comments, and discussion posts yourself.
## Giving an Assistant the Docs [#giving-an-assistant-the-docs]
If you use an assistant, give it the documentation for the version you run. Otherwise it answers from memory: 2.0 replaces nests with tags, so an assistant that remembers 1.x will tell you to create a nest that no longer exists.
### Copy a Page [#copy-a-page]
The **Copy Markdown** button under each page's title copies the whole page as Markdown, ready to paste into a chat.
The **Open** menu next to it shows the page as Markdown, or opens a chat about the page in ChatGPT, Claude, or Cursor.
### Markdown Addresses [#markdown-addresses]
Add `.md` to the end of any page's address to get it as Markdown. For example, the upgrade guide is at:
```text
https://docs.pterodactyl.io/v2/upgrading/upgrading-from-v1.md
```
Give these addresses to an assistant that can open web pages. Tools that send an `Accept: text/markdown` header also get Markdown from a page's normal address.
### The Whole Site [#the-whole-site]
Two files describe the whole site:
* [`/llms.txt`](https://docs.pterodactyl.io/llms.txt) lists every page with a link and a one-line description.
* [`/llms-full.txt`](https://docs.pterodactyl.io/llms-full.txt) contains every page, including the full API reference, in one file. At about 900 KB, it is more than many assistants can read at once.
Both files cover 1.x and 2.0, so tell the assistant which version you run. For most jobs, a few specific pages work better than the whole site.
## Prompts [#prompts]
These prompts point an assistant at the right pages and tell it to follow them. They do not make its answers correct, and everything in [You Are Responsible](#you-are-responsible) still applies.
Paste a prompt into an assistant that can open web pages. If yours cannot, open each `.md` address yourself and paste the pages after the prompt. Replace the parts in angle brackets before you send it.
### Upgrading a 1.x Panel [#upgrading-a-1x-panel]
This prompt fills in your install directory and PHP version from **Your setup**:
```text title="Upgrade prompt"
I want to upgrade my Pterodactyl Panel from 1.x to 2.0. Read these pages first, and follow them instead of what you remember about Pterodactyl:
- https://docs.pterodactyl.io/v2/upgrading/changes-from-v1.md
- https://docs.pterodactyl.io/v2/upgrading/upgrading-from-v1.md
- https://docs.pterodactyl.io/v2/upgrading/testing-the-upgrade.md
My Panel is a native installation in /var/www/pterodactyl. PHP runs as the php8.3-fpm service.
Work through the upgrade guide with me one step at a time. Before each step, tell me what it changes and how to undo it. Ask me for anything you need to know, such as my 1.x version, my web server, or whether I use a billing module. Do not suggest php artisan p:upgrade. Do not ask for my .env file or my API keys. If the guide does not cover something, say so instead of guessing.
```
### Writing an Egg [#writing-an-egg]
Before you write an egg, check whether the community catalog at [eggs.pterodactyl.io](https://eggs.pterodactyl.io) already has one. You can import it from **Admin → Eggs**.
```text title="Egg prompt"
I want to write a Pterodactyl 2.0 egg for . Read these pages first, and follow them instead of what you remember about Pterodactyl:
- https://docs.pterodactyl.io/v2/guides/egg-creation/creating-custom-egg.md
- https://docs.pterodactyl.io/v2/guides/egg-creation/egg-docker-images.md
- https://docs.pterodactyl.io/v2/guides/egg-creation/egg-config-parser.md
- https://docs.pterodactyl.io/v2/guides/egg-creation/egg-variables.md
- https://docs.pterodactyl.io/v2/guides/egg-creation/egg-install-script.md
In 2.0, eggs do not belong to a nest. Suggest tags instead.
Give me each value I need to enter for the egg: the Docker images, the startup command, the stop command, the start configuration, and the configuration files. Then give me the variables with their input rules, and the install script. If you are not sure about a value, say so instead of guessing.
```
If the game needs its own Docker image, add `https://docs.pterodactyl.io/v2/guides/egg-creation/creating-custom-image.md` to the list. Test the egg on a server you can delete before you share it.
### Building an Extension [#building-an-extension]
```text title="Extension prompt"
I want to build a Pterodactyl 2.0 extension that . Read these pages first, and follow them instead of what you remember about Pterodactyl:
- https://docs.pterodactyl.io/v2/extensions.md
- https://docs.pterodactyl.io/v2/extensions/building.md
- https://docs.pterodactyl.io/v2/extensions/backend.md
- https://docs.pterodactyl.io/v2/extensions/frontend.md
- https://docs.pterodactyl.io/v2/extensions/data-and-styling.md
- https://docs.pterodactyl.io/v2/extensions/testing-and-packaging.md
- https://docs.pterodactyl.io/v2/extensions/reference.md
Start from the php artisan p:extension:make command. Only use the manifest fields, backend helpers, SDK exports, and slots that the reference lists. If something I want is not possible with them, tell me instead of changing the Panel's own files.
```
An extension runs with full access to your Panel. Read and understand an assistant's code before you install it, as you would an extension from a stranger.
# Changes From 1.x
## Introduction [#introduction]
This page lists the 2.0 changes that affect 1.x installations, and how likely each one is to affect you. To upgrade, follow [Upgrading From 1.x](./upgrading-from-v1.mdx).
## High Impact Changes [#high-impact-changes]
### PHP 8.3 and the Intl Extension [#php-83-and-the-intl-extension]
**Likelihood Of Impact: High**
The Panel now needs PHP 8.3 or newer and the `intl` PHP extension. 1.x also ran on PHP 8.2, and the 1.x installation guide does not install `intl`. On Ubuntu, install it with:
```bash
apt install php8.3-intl
```
If `php -v` shows a version older than 8.3, upgrade PHP first with [Upgrading PHP](../guides/configuration/php-upgrade.mdx). [Requirements](../panel/requirements.mdx) has the full list.
### Upgrading With p:upgrade [#upgrading-with-pupgrade]
**Likelihood Of Impact: High**
Do not use `php artisan p:upgrade` or the 1.x update steps to move to 2.0. Both download the latest release and unpack it over your 1.x files, which leaves your Panel offline. Once 2.0 is released, `p:upgrade` without `--release` will download 2.0.
Instead, follow [Upgrading From 1.x](./upgrading-from-v1.mdx), which installs 2.0 in a new directory.
### Nest Endpoints Removed From the Application API [#nest-endpoints-removed-from-the-application-api]
**Likelihood Of Impact: High**
The `/api/application/nests` endpoints are removed and now return `404`. Use the egg and tag endpoints instead:
| 1.x | 2.0 |
| ---------------------------------------------- | ---------------------------------------------- |
| `GET /api/application/nests` | `GET /api/application/tags` |
| `GET /api/application/nests/{nest}` | `GET /api/application/tags/{tag}` |
| `GET /api/application/nests/{nest}/eggs` | `GET /api/application/eggs?filter[tag]={slug}` |
| `GET /api/application/nests/{nest}/eggs/{egg}` | `GET /api/application/eggs/{egg}` |
When the upgrade turns a nest with two or more eggs into a tag, the tag stores the nest's ID in its `legacy_nest_id` attribute, so integrations that stored nest IDs can find the matching tag. Not every nest becomes a tag; see [Review Egg Tags](./upgrading-from-v1.mdx#review-egg-tags). To get an egg's tags, add `?include=tags` to an egg request. The tag endpoints need the same read permission as the egg endpoints.
This mostly affects billing modules. If you use the official [WHMCS module](https://github.com/pterodactyl/whmcs), update it before you upgrade the Panel; the updated module works with 1.x and 2.0. For any other billing module, ask its developer whether it supports 2.0.
Existing API keys keep working, as do the other Application API requests billing modules make, such as creating, suspending, and deleting servers.
## Medium Impact Changes [#medium-impact-changes]
### The /assets/ Web Server Block [#the-assets-web-server-block]
**Likelihood Of Impact: Medium**
The 2.0 web server configuration adds a block for `/assets/`. It serves the Panel's `public/assets` directory, where themes and extensions publish their files, as plain files: it never passes them to PHP, and it refuses hidden files.
The Panel loads without the block, but add it to your configuration when you upgrade. [Switch to 2.0](./upgrading-from-v1.mdx#switch-to-20) shows it for NGINX, Apache, and Caddy. The Docker image already includes it.
### Nests Are Replaced by Tags [#nests-are-replaced-by-tags]
**Likelihood Of Impact: Medium**
Eggs are now grouped with tags instead of nests. An egg can have several tags, and nodes can have tags too. The upgrade turns your nests into tags; [Review Egg Tags](./upgrading-from-v1.mdx#review-egg-tags) explains how.
Your eggs keep working, and 2.0 can import egg files exported from 1.x.
The `nests` table stays in your database, but 2.0 no longer updates it. Eggs and servers created on 2.0 do not belong to a nest.
### Changes to 1.x Panel Files [#changes-to-1x-panel-files]
**Likelihood Of Impact: Medium**
The upgrade installs 2.0 in a new directory and copies only your `.env` file. Anything you changed or added in the 1.x Panel directory stays behind, such as an edited theme, a modified page, or files from an addon installer.
You cannot copy these changes across: 2.0 has a rebuilt frontend and a new admin area, so changes to 1.x source files do not apply. Rebuild them for 2.0 instead:
* To change colors, corners, or console colors, create a [theme](../guides/customization/themes.mdx). Themes need no frontend build.
* To add pages or features, use an [extension](../extensions/index.mdx).
Both keep working when you update the Panel.
### Database Versions [#database-versions]
**Likelihood Of Impact: Medium**
The Panel is tested with MariaDB 10.11 and 11, and MySQL 8 and 9. The 1.x minimums, MySQL 5.7 and MariaDB 10.2, are not tested with 2.0. If you run an older database server, upgrade it first.
## Low Impact Changes [#low-impact-changes]
### Admin API [#admin-api]
**Likelihood Of Impact: Low**
2.0 adds an Admin API at `/api/admin`, which the new admin area uses to manage the Panel, including eggs, tags, settings, and extensions. The Client and Application APIs keep their paths. See the [API overview](../api/index.mdx).
### Building the Frontend [#building-the-frontend]
**Likelihood Of Impact: Low**
This affects you only if you build the Panel from source, which until 2.0 is released includes everyone following [Upgrading From 1.x](./upgrading-from-v1.mdx). The frontend now builds with npm and Vite instead of Yarn and Webpack, and needs Node.js 22.12 or newer. The `--openssl-legacy-provider` option is no longer needed. The release archive will contain the built frontend.
### Docker Image [#docker-image]
**Likelihood Of Impact: Low**
The Panel Docker image now includes the queue worker and the scheduler, so you need no separate worker service or cron entry. It does not run database migrations when it starts; run them yourself in each deployment. See [Docker Deployment](../panel/docker.mdx).
# Testing the Upgrade
## Introduction [#introduction]
Try the upgrade on a copy before you upgrade your live Panel. A test run shows how long the upgrade takes and whether your eggs, integrations, and extensions still work, without affecting your users.
## Making a Safe Copy [#making-a-safe-copy]
Your copy uses real data, so it can reach real systems. Keep it isolated.
1. Back up your database and `.env` file as described in [Back Up Your Panel](./upgrading-from-v1.mdx#back-up-your-panel).
2. Restore them on a separate machine with the same 1.x version installed. Do not change your live Panel or its backup.
3. Block the copy from reaching your live Wings nodes, database hosts, mail server, backup storage, and webhooks. Your database contains their addresses and credentials.
4. Use a separate Redis server for the copy.
5. Keep the queue worker and the cron entry stopped until you have checked the database.
## Recording Your Data [#recording-your-data]
Before you upgrade the copy, record what it contains so you can compare afterwards. At a minimum, count users, servers, eggs, nodes, allocations, schedules, backups, and API keys.
## Running the Upgrade [#running-the-upgrade]
Follow [Upgrading From 1.x](./upgrading-from-v1.mdx) on the copy. Time the database migration and note any errors.
Then compare your data with the counts you recorded. They should match.
## Checking the Result [#checking-the-result]
Once the data matches, start the queue worker, bring the copy online, and check each of these:
* Sign in with an existing account, including one that uses two-factor authentication.
* Use an existing API key with the Application API and the Client API.
* Connect a test Wings node, and start, stop, and restart a test server. Open its console, files, and SFTP.
* Run a schedule and create a backup.
* Test your billing module against the copy, for example by creating, suspending, and deleting a test service.
* Enable your extensions one at a time.
## Practicing a Rollback [#practicing-a-rollback]
Finally, follow [Rolling Back](./upgrading-from-v1.mdx#rolling-back) on the copy and check that 1.x works again. Know how to roll back before you upgrade your live Panel.
For the live upgrade, plan a maintenance window, and agree in advance on the point at which you will roll back instead of fixing forward.
## For Panel Developers [#for-panel-developers]
The Panel repository includes an automated test of the database upgrade. It imports the 1.15.1 database schema into a temporary MariaDB container, adds test data, and runs the 2.0 migrations against it.
To run it, install the Composer development dependencies in a 2.x Panel checkout, build a Panel image, and run:
```bash
bash .github/docker/migration-pilot.sh pterodactyl-panel:local
```
The test uses an isolated Docker network and removes everything when it finishes. It checks that:
* An incomplete 1.x database is refused before anything changes.
* Existing records and encrypted values are unchanged after the migration.
* Nests are converted into tags.
* Running the migrations a second time changes nothing.
# Upgrading From 1.x
Pterodactyl 2.0 is not released yet. Only follow this guide on a test copy of your Panel; see [Testing the Upgrade](./testing-the-upgrade.mdx).
## Introduction [#introduction]
This guide upgrades a native 1.x Panel, installed with the [1.x installation guide](/v1/panel/getting-started), to 2.0. First read [Changes From 1.x](./changes-from-v1.mdx) to see which changes affect your Panel.
You install 2.0 in a new directory next to 1.x, migrate the database, then swap the directories. Your 1.x files stay unchanged, so you can switch back if something goes wrong. Game servers keep running; only the Panel goes offline.
Enter your install directory, PHP version, and web server below to fill them into the commands on this page.
## Upgrading Using AI [#upgrading-using-ai]
An AI assistant is a tool you can choose to use, at your own risk. If you do, give it this guide: click **Copy Markdown** at the top of this page, or point it at `https://docs.pterodactyl.io/v2/upgrading/upgrading-from-v1.md`. [Using AI With Pterodactyl](../project/using-ai.mdx) has an upgrade prompt.
You are responsible for every command you run, whoever suggested it. Check each one against this page, keep your backups, and try the upgrade on a copy first. If something breaks, the community can help with the steps on this page, not with what an assistant told you to do.
## Before You Upgrade [#before-you-upgrade]
### Update to the Latest 1.x Release [#update-to-the-latest-1x-release]
You need Panel 1.12.0 or newer. We recommend updating to the latest 1.x release first with the [1.x update guide](/v1/panel/updating).
If your database is from an older release, the upgrade stops before changing anything and asks you to update 1.x first.
### Install the Intl PHP Extension [#install-the-intl-php-extension]
Pterodactyl 2.0 needs PHP 8.3 or newer and the `intl` extension. The 1.x installation guide does not install `intl`, so add it:
```bash
apt install php8.3-intl
```
If `php -v` shows a version older than 8.3, upgrade PHP first with [Upgrading PHP](../guides/configuration/php-upgrade.mdx).
Building the frontend also needs Node.js 22.12 or newer. The steps below cover installing it.
### Update Your Billing Module [#update-your-billing-module]
Pterodactyl 2.0 removes the Application API's nest endpoints, so any billing module that requests `/api/application/nests/...` stops working after the upgrade.
If you use the official [WHMCS module](https://github.com/pterodactyl/whmcs), update it before you upgrade the Panel; the updated module works with 1.x and 2.0. For any other billing module, ask its developer whether it supports 2.0.
Do not use `php artisan p:upgrade` or the 1.x update steps to move to 2.0. They unpack 2.0 on top of your 1.x files and leave your Panel offline. Follow the steps below instead.
## Upgrade Steps [#upgrade-steps]
The commands use the `www-data` user and `pteroq` service from the 1.x installation guide, plus the install directory and PHP version from **Your setup** above. Change anything else that differs on your system.
### Enter Maintenance Mode [#enter-maintenance-mode]
Put the Panel into maintenance mode and stop the queue worker:
```bash
cd /var/www/pterodactyl
php artisan down
systemctl stop pteroq
```
### Back Up Your Panel [#back-up-your-panel]
Back up the database, your `.env` file, and the Panel directory:
```bash
mysqldump --single-transaction --routines --triggers panel > /root/panel-1.x.sql
cp /var/www/pterodactyl/.env /root/panel-1.x.env
tar -czf /root/panel-1.x-files.tar.gz -C /var/www/pterodactyl .
```
**Keep a copy of your `.env` file somewhere safe.** It holds your `APP_KEY`, without which the Panel cannot read node tokens, database passwords, two-factor secrets, or API keys.
### Download 2.0 [#download-20]
2.0 has no release yet, so download the `2.0-develop` branch and unpack it in a new directory:
```bash
mkdir /var/www/pterodactyl-2.0
cd /var/www/pterodactyl-2.0
curl -Lo panel.zip https://github.com/pterodactyl/panel/archive/refs/heads/2.0-develop.zip
unzip -q panel.zip
cp -a panel-2.0-develop/. .
rm -rf panel-2.0-develop panel.zip
```
Then copy your `.env` file from 1.x. It needs no changes:
```bash
cp /var/www/pterodactyl/.env /var/www/pterodactyl-2.0/.env
```
Do not run `php artisan key:generate`. The Panel must keep using your existing key.
### Install Dependencies [#install-dependencies]
Install the PHP dependencies with Composer:
```bash
COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader
```
If Composer reports a missing PHP extension, install it and run the command again. To check that PHP has everything the Panel needs, run:
```bash
composer check-platform-reqs --no-dev
```
The branch does not include the built frontend, so build it with Node.js 22.12 or newer. If `node --version` prints an older version or no version, first install Node.js 22 from NodeSource:
```bash
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt -y install nodejs
```
Then build the frontend:
```bash
npm ci
npm run build:production
```
### Migrate the Database [#migrate-the-database]
Put the new directory into maintenance mode too, then run the database migrations:
```bash
php artisan down
php artisan migrate --force
```
The migration took under ten seconds on our test installation. It adds tables for tags and extensions and does not change your existing data.
1.x updates also ran the database seeder to update the default eggs. In 2.0 this is optional. To update the default eggs, run:
```bash
php artisan db:seed --force
```
### Switch to 2.0 [#switch-to-20]
Give the web server user ownership of the new directory, then swap the two directories:
```bash
chown -R www-data:www-data /var/www/pterodactyl-2.0
mv /var/www/pterodactyl /var/www/pterodactyl-1.x
mv /var/www/pterodactyl-2.0 /var/www/pterodactyl
```
Next, add the `/assets/` block to the Panel's web server configuration. It serves `public/assets`, where themes and extensions publish their files, as plain files: it never passes them to PHP, and it refuses hidden files. The full configuration is on [Webserver Configuration](../panel/webserver-configuration.mdx).
Add this block inside the Panel's `server` block in `/etc/nginx/sites-available/pterodactyl.conf`, next to `location /`. If you use SSL, add it to the `server` block that listens on port 443:
```nginx
location ^~ /assets/ {
location ~ /\. {
deny all;
}
try_files $uri =404;
}
```
Then check the configuration and reload NGINX:
```bash
nginx -t
systemctl reload nginx
```
Add this block inside the Panel's `` in `/etc/apache2/sites-available/pterodactyl.conf`, after the existing `` block. If you use SSL, add it to the ``:
```apache
AllowOverride None
php_admin_flag engine off
Require all denied
```
Then check the configuration and reload Apache:
```bash
apachectl configtest
systemctl reload apache2
```
In `/etc/caddy/Caddyfile`, add the `@assets-dotfiles` and `@php` lines to the Panel's site block, and add `@php` to your existing `php_fastcgi` line:
```text
@assets-dotfiles path_regexp ^/assets/(.*/)?\.
respond @assets-dotfiles 403
@php not path /assets/*
php_fastcgi @php unix//run/php/php8.3-fpm.sock {
```
Then reload Caddy:
```bash
systemctl reload caddy
```
Finally, clear the caches and restart PHP and the queue worker:
```bash
cd /var/www/pterodactyl
php artisan optimize:clear
systemctl restart php8.3-fpm
systemctl start pteroq
```
### Exit Maintenance Mode [#exit-maintenance-mode]
When you are ready, bring the Panel back online:
```bash
php artisan up
```
## What Stays the Same [#what-stays-the-same]
You do not need to change these:
* **Server configuration.** Your cron entry and `pteroq` service work unchanged.
* **Environment file.** Your `.env` file works as is. 2.0 still reads 1.x setting names such as `CACHE_DRIVER`.
* **Wings.** Wings 1.13.2 and 1.13.3 work with the 2.0 Panel. You can upgrade Wings later.
* **Users.** Passwords, two-factor authentication, and SSH keys keep working.
* **API keys.** Existing Application and Client API keys keep working.
* **Servers.** Servers, subusers, schedules, backups, databases, and SFTP access keep working.
* **Settings.** Your Panel settings are kept.
## After Upgrading [#after-upgrading]
### Review Egg Tags [#review-egg-tags]
Pterodactyl 2.0 replaces nests with tags. The upgrade converts your nests like this:
* A nest named after a built-in game, such as Minecraft or Rust, gives its eggs that game's tag.
* A custom nest with two or more eggs becomes a tag with the same name.
* A custom nest with only one egg does not become a tag. Its egg has no tag after the upgrade.
Check your eggs after the upgrade and add any tags you need.
The `nests` table stays in your database, but 2.0 no longer updates it.
### Keep Your 1.x Files [#keep-your-1x-files]
Keep `/var/www/pterodactyl-1.x` and your backups until you are sure the upgrade worked. You need them to go back to 1.x.
## Troubleshooting [#troubleshooting]
### The Migration Stops With a Missing Migration Error [#the-migration-stops-with-a-missing-migration-error]
If your database is missing a 1.x migration, the upgrade stops with a message like this:
```text
This database is missing 1 Pterodactyl Panel 1.x migration(s), starting with 2024_07_13_091852_clear_unused_allocation_notes.
```
Nothing has been changed yet. To fix it:
1. Bring 1.x back online by running `php artisan up` and `systemctl start pteroq` in `/var/www/pterodactyl`.
2. Update 1.x to the latest 1.x release and run `php artisan migrate --force`.
3. Start this guide again from the beginning.
## Rolling Back [#rolling-back]
To go back to 1.x, swap the directories back and restore your database backup:
```bash
cd /var/www/pterodactyl
php artisan down
systemctl stop pteroq
mv /var/www/pterodactyl /var/www/pterodactyl-2.0-failed
mv /var/www/pterodactyl-1.x /var/www/pterodactyl
mysql -e 'DROP DATABASE panel; CREATE DATABASE panel;'
mysql panel < /root/panel-1.x.sql
cd /var/www/pterodactyl
php artisan cache:clear
php artisan config:clear
php artisan view:clear
systemctl restart php8.3-fpm
systemctl start pteroq
php artisan up
```
Do not use `php artisan migrate:rollback`. It cannot undo the upgrade.
Rolling back only restores the Panel. Anything 2.0 changed outside the Panel stays changed:
* **Servers created on 2.0** still exist on your Wings nodes. Delete their files by hand.
* **Database passwords changed on 2.0** no longer match. Rotate them again from 1.x.
* **Backups created on 2.0** stay in your backup storage.
Any other changes made in the Panel after the upgrade are lost.
## Known Limitations [#known-limitations]
This guide has only been tested with Panel 1.15.1, MariaDB 10.11, and Wings 1.13.2 on Ubuntu 24.04. Not yet tested:
* Large production databases.
* MySQL, and other MariaDB versions.
* Moving from a native installation to the [Docker image](../panel/docker.mdx).
* HTTPS, multiple nodes, server transfers, and S3 backups.
* Extensions.
# Configuration
## Introduction [#introduction]
Wings reads `/etc/pterodactyl/config.yml` at startup. The Panel creates it, through the auto-deploy command or as a copy from the node's **Configuration** tab. See [Configuring Wings](./installing.mdx#configuring-wings).
The Panel's file only has the keys the Panel knows about. At startup, Wings fills in every other key with its default and writes the complete file back, so the file on a running node is much longer.
After you edit the file, restart Wings:
```bash
systemctl restart wings
```
Container settings, such as DNS servers and limits, apply the next time each server starts, because Wings creates a new container on every start.
Back up the file before you edit it. If the YAML is invalid, Wings stops with `error while reading configuration file`. Indent with spaces, not tabs, and keep indentation consistent.
## How the Panel and Wings Share the File [#how-the-panel-and-wings-share-the-file]
When you save a node's settings, the Panel sends Wings an updated configuration with the tokens, ports, SSL settings, and upload limit, and Wings writes it to `config.yml`. Some parts behave differently:
* Wings listens on a new port only after it restarts.
* Wings keeps its certificate and key paths when you change the node's FQDN. Update `api.ssl.cert` and `api.ssl.key` yourself.
* `wings configure` and these updates do not copy `system.data` or `allowed_mounts`. If you changed the Daemon Base Path or use mounts, set these two keys by hand. Copying the whole file from the **Configuration** tab does include them.
* If the Panel cannot reach Wings, it saves your changes anyway and tells you to update `config.yml` yourself.
* With `ignore_panel_config_updates: true`, Wings ignores these updates, and the file only changes when you edit it.
## Panel Connection [#panel-connection]
| Key | Default | Notes |
| ------------------------------------ | ---------------- | ---------------------------------------------------------------------------------------------- |
| `remote` | Set by the Panel | The Panel's address, such as `https://panel.example.com`. Wings calls the Panel's API here. |
| `token_id` | Set by the Panel | Identifies the node to the Panel. Secret. |
| `token` | Set by the Panel | The secret Wings and the Panel share. Secret. |
| `remote_query.timeout` | `30` | Seconds Wings waits for each request to the Panel. |
| `remote_query.boot_servers_per_page` | `50` | Servers Wings requests per page at startup. Larger pages can make the Panel run out of memory. |
| `allowed_origins` | `[]` | Extra browser origins Wings accepts, besides `remote`. |
| `allow_cors_private_network` | `false` | Only for Wings without SSL on an internal IP address. |
`remote` must match the address in your browser exactly, including `https://` and with no trailing slash. Wings only accepts console connections from that origin or from one in `allowed_origins`. If you open the Panel from more than one address, list the others:
```yml
allowed_origins:
- https://panel.example.net
```
At startup, Wings fetches its servers from `remote`. If it cannot reach the Panel, it stops with `failed to load server configurations`.
### Keeping the Token Out of the File [#keeping-the-token-out-of-the-file]
`token_id` and `token` are secrets that the auto-deploy command fills in. Never share them, and remove them before you post your configuration anywhere.
To keep them out of `config.yml`, set either value to `file://` followed by a path to read it from a file, or to `${NAME}` to read an environment variable:
```yml
token: file:///etc/pterodactyl/token
```
The environment variables `WINGS_TOKEN_ID` and `WINGS_TOKEN` override both values.
## API and SSL [#api-and-ssl]
The Panel calls Wings' API to manage servers. Browsers also connect to it directly, for the console and for file uploads and downloads.
| Key | Default | Notes |
| ----------------------------- | ---------------- | ------------------------------------------------------------------------------------- |
| `api.host` | `0.0.0.0` | The address the API listens on. |
| `api.port` | `8080` | Must match the node's **Daemon Port** in the Panel. |
| `api.ssl.enabled` | `false` | The Panel writes `true` when the node uses SSL and is not behind a proxy. |
| `api.ssl.cert` | Set by the Panel | `/etc/letsencrypt/live//fullchain.pem` |
| `api.ssl.key` | Set by the Panel | `/etc/letsencrypt/live//privkey.pem` |
| `api.upload_limit` | `100` | The largest file manager upload, in MB. Set it with the node's **Upload Size Limit**. |
| `api.disable_remote_download` | `false` | Turns off downloading files from a URL in the file manager. |
| `api.trusted_proxies` | `[]` | Proxies allowed to pass on the client's address. |
If the Panel uses HTTPS, Wings must too, because browsers block insecure connections from a secure page. Wings loads its certificate at startup: if the files are missing, it stops with `failed to configure HTTPS server`, and after a renewal it needs a restart. See [Creating SSL Certificates](../guides/tutorials/ssl-certificates.mdx).
### Behind a Reverse Proxy [#behind-a-reverse-proxy]
If a proxy handles HTTPS for Wings, turn on **Behind Proxy** in the node's settings. The Panel then writes `api.ssl.enabled: false`, and Wings serves plain HTTP. The Panel and browsers still connect to `https://:`, so your proxy must accept HTTPS there and forward requests to Wings.
Add your proxy's address to `trusted_proxies` so the activity log records your users' addresses, not the proxy's:
```yml
api:
trusted_proxies:
- 192.0.2.10
```
Entries may be IP addresses or ranges such as `192.0.2.0/24`. Any other entry stops Wings at startup. SFTP does not use HTTP, so users connect to it directly, not through the proxy.
## SFTP [#sftp]
| Key | Default | Notes |
| -------------------------- | --------- | -------------------------------------------------------- |
| `system.sftp.bind_address` | `0.0.0.0` | The address the SFTP server listens on. |
| `system.sftp.bind_port` | `2022` | Must match the node's **Daemon SFTP Port** in the Panel. |
| `system.sftp.read_only` | `false` | Refuses every change over SFTP. |
Users sign in with their Panel account. The username is their Panel username, a period, and the server's 8-character ID, as shown under **SFTP Details** on the server's **Settings** page. Wings keeps its SFTP host key in the data directory, under `.sftp/`.
## Directories [#directories]
| Key | Default | Notes |
| -------------------------- | ------------------------------- | -------------------------------------------------------------- |
| `system.root_directory` | `/var/lib/pterodactyl` | Wings' own data, such as its database and each server's state. |
| `system.data` | `/var/lib/pterodactyl/volumes` | Server files, one directory per server. |
| `system.backup_directory` | `/var/lib/pterodactyl/backups` | Local backups. |
| `system.archive_directory` | `/var/lib/pterodactyl/archives` | Archives for server transfers. |
| `system.tmp_directory` | `/tmp/pterodactyl` | Scratch space for installation scripts. |
| `system.log_directory` | `/var/log/pterodactyl` | `wings.log`, and installation logs under `install/`. |
Move existing server directories before you change `system.data`, or servers whose files are still at the old path appear empty.
## System [#system]
| Key | Default | Notes |
| ---------------------------------- | ------------- | --------------------------------------------------------------------------------- |
| `system.username` | `pterodactyl` | The user that owns server files. Wings creates it if it does not exist. |
| `system.timezone` | Detected | Passed to every container. Must be a name such as `Europe/Berlin`. |
| `system.check_permissions_on_boot` | `true` | Fixes file ownership before each server starts. |
| `system.disk_check_interval` | `150` | Seconds Wings reuses a server's measured disk usage. `0` turns disk checking off. |
| `system.websocket_log_count` | `150` | Lines of earlier output a new console connection receives. |
| `system.enable_log_rotate` | `true` | Writes `/etc/logrotate.d/wings` if it does not exist. |
| `debug` | `false` | Debug logging. `wings --debug` does the same for one run. |
| `app_name` | `Pterodactyl` | The name in Wings' console messages. |
If servers with many files are slow to start at "Ensuring file permissions are set correctly", set `check_permissions_on_boot` to `false`. Wings then stops fixing files that other tools changed.
Low values for `disk_check_interval` make Wings measure every server's files often, which costs a lot of disk I/O and CPU.
### Crash Detection [#crash-detection]
When a server stops without anyone pressing **Stop**, Wings prints its exit code and whether it ran out of memory, then starts it again.
| Key | Default | Notes |
| --------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------ |
| `system.crash_detection.enabled` | `true` | Restart servers after a crash. |
| `system.crash_detection.detect_clean_exit_as_crash` | `true` | Also treat an exit with code 0 as a crash. |
| `system.crash_detection.timeout` | `60` | If a server crashes again within this many seconds, Wings does not restart it. `0` always restarts it. |
### Backups and Transfers [#backups-and-transfers]
| Key | Default | Notes |
| --------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `system.backups.write_limit` | `0` | Maximum backup write speed, in MiB/s. Also applies to S3 backups, which are written locally first. `0` means no limit. |
| `system.backups.compression_level` | `best_speed` | `none`, `best_speed`, or `best_compression`. |
| `system.backups.restore_host_allowlist` | `[]` | Private or internal hosts that backup restores may download from. |
| `system.transfers.download_limit` | `0` | Maximum speed at which this node downloads a transferred server, in MiB/s. `0` means no limit. |
## Docker [#docker]
### Network [#network]
On first start, Wings creates a Docker network named `pterodactyl_nw`, with the bridge `pterodactyl0`. If the network already exists, Wings uses it as is.
| Key | Default | Notes |
| ------------------------------------- | --------------------- | ------------------------------------------------------------------- |
| `docker.network.dns` | `1.1.1.1`, `1.0.0.1` | DNS servers for every container. |
| `docker.network.name` | `pterodactyl_nw` | The network's name. |
| `docker.network.network_mode` | `pterodactyl_nw` | The network containers join. |
| `docker.network.interface` | `172.18.0.1` | The network's gateway. Allocations on `127.0.0.1` are mapped to it. |
| `docker.network.interfaces.v4.subnet` | `172.18.0.0/16` | Only used when Wings creates the network. |
| `docker.network.interfaces.v6.subnet` | `fdba:17c8:6c94::/64` | Only used when Wings creates the network. |
| `docker.network.enable_icc` | `true` | Lets containers reach each other. |
| `docker.network.network_mtu` | `1500` | The bridge's MTU. |
If servers cannot resolve domain names, your host may block the default DNS servers. Replace them with your host's, as described in [Servers Have No Internet Access](../panel/troubleshooting.mdx#servers-have-no-internet-access).
To change the subnet, stop every server on the node, then remove the network so Wings creates it again. Docker cannot remove a network that running containers still use.
```bash
systemctl stop wings
docker network rm pterodactyl_nw
systemctl start wings
```
To give containers the host's network directly, set both `name` and `network_mode` to `host`:
```yml
docker:
network:
name: host
network_mode: host
```
Host networking removes Docker's network isolation. Every server can then listen on any address and port of the node, not only on its own allocations. Do not use it on nodes that host servers for others.
### Private Registries [#private-registries]
To pull images from a registry that needs a login, add it under `docker.registries`, keyed by the registry's address:
```yml
docker:
registries:
registry.example.com:
username: registryuser
password: registrypassword
```
Wings uses the entry whose address matches the image's registry.
### Container Limits [#container-limits]
| Key | Default | Notes |
| -------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------- |
| `docker.tmpfs_size` | `100` | The size of `/tmp` in each container, in MB. It uses host memory and does not count toward the server's limit. |
| `docker.container_pid_limit` | `512` | The most processes a container may run. `0` removes the limit, which we do not recommend. |
| `docker.installer_limits.memory` | `1024` | Memory for installation containers, in MB, if higher than the server's own limit. |
| `docker.installer_limits.cpu` | `100` | CPU for installation containers, in percent, if higher than the server's own limit. |
| `docker.overhead.override` | `false` | Use your own memory overhead instead of the defaults. |
Programs like Java use more memory than their configured heap, so Docker allows each container some memory above the server's limit. By default, Wings adds 15% for servers with up to 2048 MB, 10% up to 4096 MB, and 5% above that. To use your own steps, set `override: true`:
```yml
docker:
overhead:
override: true
default_multiplier: 1.05
multipliers:
2048: 1.15
4096: 1.10
```
### CPU [#cpu]
These settings only affect servers with a CPU limit.
| Key | Default | Notes |
| -------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `docker.cpu_period` | `100000` | The CPU scheduling window, in microseconds. Limits scale with it. |
| `docker.cpu_burst.enabled` | `true` | Lets a server spend saved CPU time on short spikes. Needs Linux 5.14 or newer. |
| `docker.cpu_burst.percent` | `100` | How much a server may burst, relative to its limit. |
| `docker.cpu_shares` | `0` | The server's weight when the CPU is fully busy. `0` uses Docker's default. `1024` restores the behavior of older Wings versions, which favors the host's own services. |
## Console Throttling [#console-throttling]
Wings limits how fast a server may print to its console, so a runaway process cannot flood Wings and the browser. When a server prints more than `lines` lines within `line_reset_interval` milliseconds, Wings drops the extra output and prints "Server is outputting console data too quickly -- throttling...".
| Key | Default | Notes |
| ------------------------------- | ------- | ------------------------------ |
| `throttles.lines` | `2000` | Lines allowed per interval. |
| `throttles.line_reset_interval` | `100` | The interval, in milliseconds. |
Wings writes a `throttles.enabled` key, but does not read it. Throttling is always on; raise `lines` to loosen it.
## Mounts [#mounts]
`allowed_mounts` lists the host directories that servers may mount. Wings skips a mount whose source is outside these, and logs `skipping custom server mount, not in list of allowed mount points`. See [Using Mounts](../guides/configuration/mounts.mdx).
# Installing Wings
## Introduction [#introduction]
Wings runs on each node, manages Docker, and runs your game servers for the Panel. For the connections and ports it uses, see [How Pterodactyl Works](../project/how-it-works.mdx).
Pterodactyl 2.0 uses the same Wings as 1.x, and Wings 1.13.2 and 1.13.3 are tested with it.
## Supported Systems [#supported-systems]
Wings supports these operating systems:
| Operating System | Versions |
| ---------------- | ------------ |
| Ubuntu | 22.04, 24.04 |
| Debian | 11, 12, 13 |
Other Linux distributions may work but often need changes to these steps, and we cannot help with them. Wings does not run on Windows.
## System Requirements [#system-requirements]
Wings needs a Linux system that can run Docker containers. Most virtual private servers and nearly all dedicated servers can.
If your provider uses `Virtuozzo`, `OpenVZ`, or `LXC` virtualization, Wings probably will not run. Some providers support Docker anyway, so ask them. KVM always works.
To check your virtualization, run:
```bash
systemd-detect-virt
```
If the result is not `openvz` or `lxc`, Wings should work. `none` means the server is not virtualized.
## Dependencies [#dependencies]
Wings needs `curl` and Docker.
### Installing Docker [#installing-docker]
Install Docker with Docker's install script:
```bash
curl -sSL https://get.docker.com/ | CHANNEL=stable bash
```
To install it by hand, follow the [official Docker documentation](https://docs.docker.com/engine/install/).
Some hosts use a modified kernel that lacks important Docker features. If `uname -r` ends in `-xxxx-grs-ipv6-64` or `-xxxx-mod-std-ipv6-64`, ask your host to switch to your distribution's standard kernel.
#### Start Docker on Boot [#start-docker-on-boot]
On systemd systems, run this so Docker starts on boot:
```bash
sudo systemctl enable --now docker
```
#### Enabling Swap [#enabling-swap]
Linux kernel 6.1 and newer enable swap accounting by default, so you can skip this step. Check your version with `uname -r`.
On older kernels, Docker usually cannot limit swap, and `docker info` prints `WARNING: No swap limit support` near the end. Enabling swap is optional, but we recommend it if you host servers for others, to help prevent out-of-memory errors.
To enable it, open `/etc/default/grub` as root and add `swapaccount=1` inside the quotes of the `GRUB_CMDLINE_LINUX_DEFAULT` line, keeping anything already there:
```text
GRUB_CMDLINE_LINUX_DEFAULT="swapaccount=1"
```
Then run `sudo update-grub` and `sudo reboot`. Some distributions ignore `GRUB_CMDLINE_LINUX_DEFAULT`. If the change has no effect, add the option to `GRUB_CMDLINE_LINUX` instead.
## Installing Wings [#installing-wings]
Create the configuration directory, then download the Wings binary for your CPU:
```bash
sudo mkdir -p /etc/pterodactyl
curl -L -o /usr/local/bin/wings "https://github.com/pterodactyl/wings/releases/latest/download/wings_linux_$([[ "$(uname -m)" == "x86_64" ]] && echo "amd64" || echo "arm64")"
sudo chmod u+x /usr/local/bin/wings
```
## Configuring Wings [#configuring-wings]
Create a node in the Panel, then give its configuration to Wings.
1. In the Panel, open **Admin → Nodes** and click **New node**.
2. Fill in the node's details, then save it.
3. Open the node and select the **Configuration** tab.
The tab shows the node's configuration file. Configure Wings in one of two ways.
### Using the Auto-Deploy Command [#using-the-auto-deploy-command]
Click **Generate Token** under **Auto-Deploy**. The Panel shows a command like this:
```bash
cd /etc/pterodactyl && sudo wings configure --panel-url https://panel.example.com --token ptla_... --node 1
```
Run it on the node. It saves the configuration from the Panel to `/etc/pterodactyl/config.yml`. If the file exists, add `--override` to replace it.
### Copying the Configuration File [#copying-the-configuration-file]
Or copy the file from the **Configuration** tab to `/etc/pterodactyl/config.yml` on the node. The tab fills in `remote` from the address you opened the Panel with, so check that it is your Panel's address.
If your Panel uses HTTPS, Wings also needs a certificate for its own domain name.
## Starting Wings [#starting-wings]
Start Wings in debug mode to check that it works:
```bash
sudo wings --debug
```
The first start may take a few minutes while Wings sets up Docker. Once it runs without errors, stop it with `CTRL+C` and run it as a service.
### Running Wings as a Service [#running-wings-as-a-service]
To run Wings in the background, create `wings.service` in `/etc/systemd/system`:
```text
[Unit]
Description=Pterodactyl Wings Daemon
After=docker.service
Requires=docker.service
PartOf=docker.service
[Service]
User=root
WorkingDirectory=/etc/pterodactyl
LimitNOFILE=4096
PIDFile=/var/run/wings/daemon.pid
ExecStart=/usr/local/bin/wings
Restart=on-failure
StartLimitInterval=180
StartLimitBurst=30
RestartSec=5s
[Install]
WantedBy=multi-user.target
```
Then enable and start it:
```bash
sudo systemctl enable --now wings
```
## Adding Allocations [#adding-allocations]
An allocation is an IP address and port you can assign to a server. Every server needs at least one. To add them, open the node in **Admin → Nodes** and select the **Allocation** tab.
Use the IP address of the node's network interface, or the internal IP address behind NAT. To find it, run:
```bash
hostname -I | awk '{print $1}'
```
Do not use `127.0.0.1` for allocations players connect to. It is only for servers that other servers on the node connect to, such as the backend servers of a [Minecraft proxy network](../guides/games/minecraft.mdx).
# Upgrading Wings
## Introduction [#introduction]
Upgrading Wings takes less than a minute. Your running game servers are not affected.
## Wings Version Requirements [#wings-version-requirements]
There is no Wings version table for Pterodactyl 2.0 yet. Wings 1.13.2 and 1.13.3 are tested with the 2.0 Panel. See [Requirements](../panel/requirements.mdx#wings).
## Downloading the New Version [#downloading-the-new-version]
Stop Wings, then download the new binary into `/usr/local/bin`:
```bash
systemctl stop wings
curl -L -o /usr/local/bin/wings "https://github.com/pterodactyl/wings/releases/latest/download/wings_linux_$([[ "$(uname -m)" == "x86_64" ]] && echo "amd64" || echo "arm64")"
chmod u+x /usr/local/bin/wings
```
## Restarting Wings [#restarting-wings]
Start Wings again:
```bash
systemctl restart wings
```
Servers keep running, and open console connections reconnect on their own.
# Creating a New Node
## Location [#location]
Head to the admin panel and click the Nodes tab on the left sidebar. After that, click 'Create New' on the top right side to open the page to add a node.
## Information Required [#information-required]
* **Name**: a quick identifiable name for the node.
* **Description**: a long description that is used to help you identify the node.
* **Location**: the location you want the node in. These are configured in the 'Locations' section of the panel and one must be created before a node can be created. These simply act as categories for nodes and serve no other purpose at this time.
* **FQDN**: the fully qualified domain name for the node — for example: `node.pterodactyl.io`
* **Communicate over SSL**: if the panel is using SSL the Daemon is required to use SSL as well.
* **Behind Proxy**: if you have the Daemon behind a proxy that terminates SSL connections before arriving at the Daemon then this option should be selected. If none of that sentence made sense, this doesn't affect you.
* **Server File Directory**: the location on the physical server where the daemon is to store the files the servers generate. By default this is `/var/lib/pterodactyl/volumes`.
Some OVH users regularly have their `/home` folder be the largest filesystem. You may want to change to use `/home/pterodactyl/volumes` if you are on a default OVH box.
* **Total Memory**: the total amount of RAM the node should be able to allocate automatically.
* **Memory Overallocate**: the percentage of RAM to over-allocate on a node. For example, if you have set a 10GB memory limit, with a 20% overallocation, the Panel will allocate up to 12GB of memory on this node in total.
* **Total Disk Space**: the total amount of disk space the node should be able to allocate automatically.
* **Disk Overallocate**: works the same way as memory overallocation.
Don't forget to account for OS overhead and other software requirements on machines.
* **Daemon Port**: the port that the Daemon should listen on.
* **Daemon SFTP Port**: the port the Daemon sftp-server or standalone SFTP server should listen on.
## Install the Daemon [#install-the-daemon]
At this point you'll need to have the Daemon installed on your machine. Check out the [documentation](/v1/wings/installing) for more information, or try one of the community guides for [CentOS](/v1/guides/wings-installation/centos7), or [Debian](/v1/guides/panel-installation/debian).
## Configuring the Node [#configuring-the-node]
Go to the Node Configuration page
Copy and paste the config into the `config.yml` file. (Default location is `/etc/pterodactyl/config.yml`)
### Auto-Deploy [#auto-deploy]
This will generate a command to run on the node server to configure the daemon for you.
# Using Mounts
Mounts is a feature that allows administrators to mount other directories from the host file-system into a Server's container.
## Wings Configuration [#wings-configuration]
For security reasons it is not possible to mount directories on a node by default. Directories that should be mountable have to be specified explicitly in the Wings configuration.
In the Wings configuration file (`/etc/pterodactyl/config.yml`) the `allowed_mounts` field is used to list mountable directories. The listed directories and all their subdirectories can be mounted.
```yml
allowed_mounts:
- /example
```
You have to restart Wings to apply new changes to your Wings config.
## Panel Configuration [#panel-configuration]
You have to configure mounts in admin Panel in order to use them with your servers. They consist of a source pad on the node and a target path where it will be mounted in the container.
Mounts can be mounted to or inside of `/home/container` or any subdirectory of it. You can cross-mount servers such as Server A's directory into Server B. Keep in mind that the folder you want to mount into needs to exist for the mount to work.
### Creating a Mount [#creating-a-mount]
1. In the admin Panel go to **Mounts**.
2. Create a new mount.
3. Fill in the details as required.
* **Name**: Name for your mount.
* **Description**: Description for your mount.
* **Source**: The absolute path to the folder or files on the Node machine.
* **Target**: The absolute path where the mount will be placed inside of your server. If `/home/container` is in the path, make sure the folder exists beforehand.
* **Read Only**: Whether the mount will be read-only for the servers using it.
* **User Mountable**: Whether to allow users to self mount this mount.
4. After creating the mount, you are required to add both **Eggs** and **Nodes** that this mount may be used on.
All servers using the same mounts will **only** share their contents when they are on the same node. Mounts are not synchronized between nodes.
### Assigning a Mount to a Server [#assigning-a-mount-to-a-server]
1. In the admin Panel navigate to the server you would like to use a mount with
2. Go to the mounts page
3. Click the **+** button
4. Restart the server
The files of the mount should become available in the target path in the container. You can temporarily change your server startup command to `ls `, which should output the contents of the mount if configured correctly.
Mounts do not appear in the Panel's file manager, nor are they accessible via SFTP. However, the server itself will be able to see and use the mounts.
### Example Mount [#example-mount]
The example mount below is stored in the path `/var/lib/pterodactyl/mounts`, which we add to the Wings `config.yml`
```yml
allowed_mounts:
- /var/lib/pterodactyl/mounts
```
# Upgrading PHP
This documentation includes instructions for upgrading your system to the latest version of PHP. Please reference the table below to check what version you need for your version of Pterodactyl.
| Panel Version | PHP Version |
| --------------- | ------------- |
| 1.0.0 - 1.2.0 | 7.3, 7.4 |
| 1.3.0+ | 7.4, 8.0 |
| 1.8.0+ | 7.4, 8.0, 8.1 |
| 1.11.0 - 1.11.3 | 8.0, 8.1 |
| 1.11.4+ | 8.1, 8.2, 8.3 |
| 1.11.10+ | 8.2, 8.3 |
## Install PHP [#install-php]
In order to install PHP 8.3, you will need to run the following command. Please keep in mind different operating systems may have slightly different requirements for how this command is formatted.
```bash
# Add additional repository for PHP
add-apt-repository -y ppa:ondrej/php
apt -y update
apt -y install php8.3 php8.3-{cli,gd,mysql,common,mbstring,tokenizer,bcmath,xml,fpm,curl,zip}
```
## Update Composer [#update-composer]
As of `Panel@1.3.0` we require `composer` v2. To update composer you will need to run the following command which will perform the composer self-update process and move you over to version 2.
```bash
composer self-update --2
```
## Webserver Configuration [#webserver-configuration]
After upgrading to PHP 8.3, you will most likely need to update your NGINX configuration. Your configuration file is most likely called `pterodactyl.conf` and located in the `/etc/nginx/sites-available/` directory, or if on CentOS, `/etc/nginx/conf.d/`.
Make sure to update the path in the command below to reflect the actual location of your configuration file.
```bash
sed -i -e 's/php[7|8].[0-9]-fpm.sock/php8.3-fpm.sock/' /etc/nginx/sites-available/pterodactyl.conf
```
Once you have edited the file run the command below to reload nginx and apply your changes.
```bash
systemctl reload nginx
```
Run the commands below to disable all previous PHP versions and enable PHP 8.3 when serving requests.
```bash
# Hint: a2dismod = a2_disable_module
a2dismod php*
# Hint: a2enmod = a2_enable_module
a2enmod php8.3
```
### Return to the 1.X.X Upgrade Guide [#return-to-the-1xx-upgrade-guide]
After completing the PHP upgrade, return to the [Panel upgrade guide](/v1/panel/updating) to continue with the update process.
# Building Panel Assets
Do **not** run the following steps on your production nodes.
Instructions on how to build the panel are also available in the [BUILDING.md](https://github.com/pterodactyl/panel/blob/develop/BUILDING.md) file.
The frontend of the Panel is built with React. Any changes to the source files require to recompile it. This also applies to style sheets. The following sections explain how to do so.
## Install Dependencies [#install-dependencies]
The following commands will install the necessary dependencies for building the Panel assets.
The build tools require NodeJS, yarn is used as the package manager.
```bash
# Ubuntu/Debian
curl -sL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
# CentOS
curl -sL https://rpm.nodesource.com/setup_22.x | sudo -E bash -
sudo yum install -y nodejs yarn # CentOS 7
sudo dnf install -y nodejs yarn # CentOS 8, Rocky Linux 8, AlmaLinux 8
```
Install required javascript packages.
```bash
npm i -g yarn # Install Yarn
cd /var/www/pterodactyl
yarn # Installs panel build dependencies
```
## Build Panel Assets [#build-panel-assets]
The following command will rebuild the Panel frontend. For NodeJS version 17 and above, you must enable the `--openssl-legacy-provider` option before building.
```bash
cd /var/www/pterodactyl
export NODE_OPTIONS=--openssl-legacy-provider # for NodeJS v17+
yarn build:production # Build panel
```
You can use command `yarn run watch` to view the progress of your changes in almost real-time for easier development. Once you're satisfied with your changes build the panel using the previously mentioned `yarn build:production` command.
# Building Wings
Do **not** run the following steps on your production nodes.
Wings is written in Go. This makes it very easy to modify and compile it on your own, and distribute your own binaries. This guide will cover the steps necessary to build it yourself.
It will not, however, explain where to look for certain aspects of Wings and which changes are necessary to achieve specific results. Knowledge of the Go language is required if you want to modify it.
Building Go programs is very easy, and the same also applies to Wings. Go is cross-platform, but Wings only supports Linux at the moment. The easiest way to compile it for Linux is to run the commands on a Linux machine.
## Build Requirements [#build-requirements]
An up to date version of Go is required to compile Wings. The minimum version can be found at the top of the [go.mod](https://github.com/pterodactyl/wings/blob/develop/go.mod) file. See the [official instructions](https://golang.org/doc/install) for help with installing Go.
## Building [#building]
Execute the following command in your local clone of the repository to compile Wings into a binary.
```bash
go build
```
You should now have a `wings` binary file in your wings directory.
## Install the new binary [#install-the-new-binary]
Some the following commands require root permissions. Prepend them with `sudo` if you are not logged in as root.
1. Backup the current installation of wings
```bash
mv /usr/local/bin/wings /usr/local/bin/wings-backup
```
2. Place the new binary in `/usr/local/bin`
```bash
cp ./wings /usr/local/bin
```
3. Restart wings
```bash
systemctl restart wings
```
## Troubleshooting [#troubleshooting]
If the wings service does not start properly, you can try to start Wings in a console window.
```bash
wings --debug
```
Remember to stop the system service before, and re-enable it afterwards.
```bash
systemctl stop wings
systemctl start wings
```
# Creating a Custom Egg
Do not modify the default Nests or Eggs provided by the Panel. Each Pterodactyl update may alter these defaults and override your changes.
Before creating a custom Egg, it's important to understand Pterodactyl's structure:
**Nests** are collections of eggs (like categories for related server types), and **Eggs** are the specific configurations for installing and running a particular game or software.
## Creating a New Nest (Category) [#creating-a-new-nest-category]
If your egg doesn't fit into an existing Nest, you should create a new Nest for it. The Nests page can be found in the Admin Dashboard (in the left sidebar, near the bottom).
Click the **"Create New"** button on the Nests page to create a new Nest. You will need to provide:
* **Name:** A descriptive name for the category (for example, "Custom Games" or "Minecraft Mods").
* **Description:** (Optional) Details about what eggs in this nest are for.
Once these are filled, save the new Nest.
If you have a pre-made Egg JSON file (for example, from the community), you can import it instead of creating one from scratch. Use the **"Import Egg"** button on the Nests page, select the target Nest, and upload the egg's JSON file. Check out the [Community Egg Repository](https://pterodactyleggs.com) for a large selection of ready-to-use community eggs.
## Creating a New Egg [#creating-a-new-egg]
After you have a Nest, you can create a new Egg within that nest. To do this, navigate to the nest's detail page, then click the **"New Egg"** button at the bottom of the page.
This opens the egg configuration form (the "New Egg" page). If not already selected, choose the appropriate Nest from the **Associated Nest** dropdown at the top of the form.
Now fill out the details for your new Egg:
### Basic Details [#basic-details]
* **Name:** The name of the Egg (e.g. "MyGame Dedicated Server"). This is how the egg will be listed in the panel.
* **Description:** A short description of what this egg is or does.
### Docker Images [#docker-images]
Here you select the Docker image(s) that the server will run in. Docker images define the environment (OS and software) available to your server. Pterodactyl provides a variety of [Official images](https://github.com/Ptero-Eggs/yolks) (called "yolks") for many common use-cases, or you can specify a custom image. See the [Egg Docker Images](/v1/guides/egg-creation/egg-docker-images) page for more details on available images.
Make sure the Docker image contains all the required runtime dependencies for your server. Any package installed **only in the install script** will **not** be present when the server is running. Also, **the image must include a user named `container` with home directory `/home/container`** (this is required by Pterodactyl for all images).
After filling in the basic details, you'll want to configure the remaining parts of your egg:
* [Configuration Files & Startup Detection](/v1/guides/egg-creation/egg-config-parser) — process management, stop commands, config file parsing, and startup detection
* [Egg Variables](/v1/guides/egg-creation/egg-variables) — define custom and default variables
* [Egg Install Script](/v1/guides/egg-creation/egg-install-script) — write the install script that sets up server files
* [Creating a Custom Docker Image](/v1/guides/egg-creation/creating-custom-image) — build your own image if the official ones don't fit your needs
# Creating a Custom Docker Image
All Pterodactyl containers must set `USER=container` and `HOME=/home/container` as environment variables, with `/home/container` as the working directory.
This guide explains how to create a custom Docker image for use with Pterodactyl eggs, using a modern base example from the [Pterodactyl yolks repository](https://github.com/pterodactyl/yolks/tree/main).
Docker images define the environment in which a server runs — what software is installed, what versions are used, and how everything is launched. Custom images allow you to add packages or modify behavior beyond what official yolks provide.
## Dockerfile Example [#dockerfile-example]
Here's a full Dockerfile example using Java 21 as the base image:
```dockerfile
FROM --platform=$TARGETOS/$TARGETARCH eclipse-temurin:21-jdk-jammy
LABEL author="Matthew Penner" maintainer="matthew@pterodactyl.io"
LABEL org.opencontainers.image.source="https://github.com/pterodactyl/yolks"
LABEL org.opencontainers.image.licenses=MIT
ENV DEBIAN_FRONTEND=noninteractive
RUN apt update -y \
&& apt install -y \
curl \
lsof \
ca-certificates \
openssl \
git \
tar \
sqlite3 \
fontconfig \
tzdata \
iproute2 \
libfreetype6 \
tini \
zip \
unzip
ENV USER=container HOME=/home/container
WORKDIR /home/container
COPY --chmod=755 ./entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/usr/bin/tini", "-g", "--"]
CMD ["/entrypoint.sh"]
```
## Breakdown of the Dockerfile [#breakdown-of-the-dockerfile]
### Base Image [#base-image]
```dockerfile
FROM --platform=$TARGETOS/$TARGETARCH eclipse-temurin:21-jdk-jammy
```
Uses Eclipse Temurin Java 21 JDK on Ubuntu Jammy. The `--platform` flag ensures compatibility with different system architectures.
### Metadata Labels [#metadata-labels]
```dockerfile
LABEL author="..."
```
Provides metadata such as author, source, and license. Useful for documentation.
### Dependencies [#dependencies]
```dockerfile
RUN apt update -y && apt install -y [...]
```
Installs useful server packages:
* `curl`, `lsof`, `openssl`: Common CLI tools.
* `fontconfig`, `libfreetype6`: Support Java-based GUI rendering.
* `tini`: Handles signal forwarding and zombie reaping.
### Environment & Working Directory [#environment--working-directory]
```dockerfile
ENV USER=container HOME=/home/container
WORKDIR /home/container
```
Sets the required environment variables and default working directory.
### Entrypoint Setup [#entrypoint-setup]
```dockerfile
COPY --chmod=755 ./entrypoint.sh /entrypoint.sh
```
Copies the startup script and ensures it's executable.
### Entry Command [#entry-command]
```dockerfile
ENTRYPOINT ["/usr/bin/tini", "-g", "--"]
CMD ["/entrypoint.sh"]
```
Uses `tini` to launch the container and execute the custom startup script.
## Example entrypoint.sh [#example-entrypointsh]
In order to complete this Dockerfile, we will need an `entrypoint.sh` file which tells Docker how to run this specific server type.
These entrypoint files are actually fairly abstracted, and the Daemon will pass in the start command as an environment variable before processing it and then executing the command.
```bash
#!/bin/bash
# Default the TZ environment variable to UTC.
TZ=${TZ:-UTC}
export TZ
# Set environment variable that holds the Internal Docker IP
INTERNAL_IP=$(ip route get 1 | awk '{print $(NF-2);exit}')
export INTERNAL_IP
# Switch to the container's working directory
cd /home/container || exit 1
# Print Java version
printf "\033[1m\033[33mcontainer@pterodactyl~ \033[0mjava -version\n"
java -version
# Convert all of the "{{VARIABLE}}" parts of the command into the expected shell
# variable format of "${VARIABLE}" before evaluating the string and automatically
# replacing the values.
PARSED=$(echo "${STARTUP}" | sed -e 's/{{/${/g' -e 's/}}/}/g' | eval echo "$(cat -)")
# Display the command we're running in the output, and then execute it with the env
# from the container itself.
printf "\033[1m\033[33mcontainer@pterodactyl~ \033[0m%s\n" "$PARSED"
# shellcheck disable=SC2086
eval ${PARSED}
```
### Breakdown [#breakdown]
* Navigates to the working directory.
* Outputs the Java version for confirmation.
* Processes and expands the Pterodactyl `STARTUP` variable.
* Executes the startup command.
## Final Notes [#final-notes]
* Must set `ENV USER=container HOME=/home/container` and `WORKDIR /home/container`.
* Build and test locally with: `docker build -t my-image .`
* Push images to Docker Hub or a private registry and specify them in the egg under Docker Image, or run the image locally first if you want to ensure it functions as expected.
# Configuration Files & Startup Detection
After setting the basic egg details, you need to configure how Wings will manage the server process. This includes defining how to start and stop the server, how to handle logs, and how to automatically update any configuration files. These settings are found on the egg's configuration page (under "Process Management").
## Startup Command [#startup-command]
The **Startup Command** is the exact command that will run to start your server. This is executed every time the server is launched (when the user clicks the Start button). You can include egg variables in this command by using the `{{VARIABLE}}` syntax. For example, a startup command might look like:
```
java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}
```
## Stop Command [#stop-command]
The **Stop Command** is the command or signal used to safely stop the server. By default, many eggs use `^C` (Control+C) to send an interrupt signal, which works for most console-based servers. If the application has a special shutdown command (for example, typing "stop" in a Minecraft server console), you can specify that here instead. Wings will execute this command when a user clicks the Stop button.
## Log Configuration [#log-configuration]
In most cases you can leave the **Log Configuration** field blank (which defaults to an empty JSON `{}`). By default, Wings will stream all output from the container's console. The log configuration field is used only in advanced cases where the server's output needs special handling (for example, if the server writes logs only to a file and not to STDOUT, you could configure Wings to tail that file). For typical eggs, this field remains empty.
## Configuration Files [#configuration-files]
Using configuration file parsing is generally an advanced feature. If you are new to creating eggs, you may skip this section unless your egg needs it.
This section allows you to define files that Pterodactyl should automatically modify each time the server starts, to ensure certain settings are always applied. You can provide a JSON object mapping file names to the values that should be set in those files. Wings will then parse and update those files before the server fully starts.
For example, consider a game that uses a `server.properties` file for its settings. You might add a configuration entry like this:
```json
{
"server.properties": {
"parser": "properties",
"find": {
"server-ip": "0.0.0.0",
"server-port": "{{server.build.default.port}}",
"max-players": "{{env.MAX_PLAYERS}}"
}
}
}
```
Each time the server starts, Wings checks if `server.properties` exists:
* If it exists, Wings will update those keys to the defined values (inserting the key if it's missing).
* If the file doesn't exist, Wings will create it with those keys and values.
A more advanced example using a YAML file and wildcards:
```json
{
"config.yml": {
"parser": "yaml",
"find": {
"listeners[0].query_enabled": true,
"listeners[0].query_port": "{{server.build.default.port}}",
"listeners[0].host": "0.0.0.0:{{server.build.default.port}}",
"servers.*.address": {
"127.0.0.1": "{{config.docker.interface}}",
"localhost": "{{config.docker.interface}}"
}
}
}
}
```
## Parser Types [#parser-types]
The available Parser Types are:
| Type | Description |
| ------------ | ------------------------------------------------------- |
| `properties` | `.properties` files with key=value pairs |
| `ini` | Supports `[sections]` and `key=value` pairs |
| `yaml` | Handles nested keys, supports wildcards |
| `json` | Parses full structure, adds missing keys |
| `xml` | Can update attributes/values via xpath |
| `file` | Simple find/replace by line content (avoid if possible) |
## Startup Configuration [#startup-configuration]
The Startup Configuration (sometimes called "startup detection") is a JSON block where you define text that indicates when the server has finished starting up. This helps Pterodactyl know when to mark the server as "Online" (running) versus "Starting".
The `done` value can be a single string or an array of strings:
```json
{
"done": "Done ("
}
```
or with multiple possible strings:
```json
{
"done": [
"Done (",
"Server is ready"
]
}
```
When any of the strings appear in the console output, Wings will consider the server to be fully started. This helps avoid marking the server as "running" before it's actually ready.
If this section is omitted, Wings relies on process status or timeout heuristics.
## Copy Settings From [#copy-settings-from]
The **Copy Settings From** option in an egg allows inheriting settings from another egg. This is useful for reducing duplication (e.g., different Minecraft flavors like Vanilla, Spigot, etc.).
If you select another egg as a parent in "Copy Settings From," any field in the current egg that you leave blank will be inherited from the parent egg. This is very useful when you have multiple eggs that share most of their configuration (for example, different versions or mods of the same game).
Please note that Copy Settings From does not support nested copies — you can only copy from a single parent, and that parent **must not be copying from another option**.
# Egg Docker Images
The [Pterodactyl yolks repository](https://github.com/pterodactyl/yolks) provides a variety of Docker images (called yolks) specifically designed for use with Pterodactyl eggs.
These images provide the necessary runtime environments for game servers, bots, utilities, databases, and other services.
Pterodactyl eggs run within Docker containers.
The **Docker image** for an egg defines the base operating system and software environment available to the server. Choosing the right image is important:
* Pterodactyl maintains an official repository of images (called **Yolks**) covering many common games, languages, and services.
* You may also use a [Custom Docker Image](/v1/guides/egg-creation/creating-custom-image) for unique requirements.
## Categories of Yolks [#categories-of-yolks]
### General Purpose [#general-purpose]
| Image | Description |
| ------------ | ---------------------------------------------------------------------------------------------------------------- |
| `oses` | Base operating system images used to build other yolks. Includes core utilities for most container environments. |
| `installers` | Includes tools like `curl` and `wget`, commonly used to simplify and speed up installation scripts. |
### Programming Languages [#programming-languages]
| Image | Description |
| -------- | ------------------------------------------------------------------------------------- |
| `go` | An environment for Go (Golang) applications. Used for servers or tools written in Go. |
| `java` | Supports running Java applications, including Minecraft servers and Java-based tools. |
| `nodejs` | Provides Node.js and npm for JavaScript-based apps like bots, utilities, etc. |
| `python` | Used to run or build Python applications, scripts, or automation tools. |
| `rust` | Provides an environment for building or running applications developed in Rust. |
### Databases [#databases]
| Image | Description |
| ---------- | ----------------------------------------------------------------------------------- |
| `mariadb` | A drop-in replacement for MySQL, used for web apps and game server databases. |
| `mongodb` | A NoSQL database suited for dynamic data structures and fast performance. |
| `postgres` | Relational SQL database known for advanced features and data integrity. |
| `redis` | In-memory data structure store, used for caching and high-performance applications. |
### Game Tools [#game-tools]
| Image | Description |
| ---------- | ---------------------------------------------------------------------------------------------------- |
| `steamcmd` | Allows downloading and managing game servers from Steam (e.g. ARK, CS:GO, Valheim). |
| `wine` | Runs Windows-based applications in Linux containers — useful for games that don't have Linux builds. |
### Other [#other]
| Image | Description |
| ------- | ---------------------------------------------------------------------------------------------------- |
| `mono` | Environment for .NET applications using the Mono runtime. Supports C# programs and older .NET games. |
| `voice` | Optimized for voice servers or tools like TeamSpeak or Mumble. |
## Architecture Support [#architecture-support]
Most yolks support both `amd64` and `arm64` architectures. Always check the image documentation to confirm compatibility with your server hardware.
## Custom Images [#custom-images]
Requirements:
* Must include all runtime dependencies.
* Must set `ENV USER=container HOME=/home/container` and `WORKDIR /home/container`.
* Use Alpine/Debian minimal bases where possible.
If you want to build your own Docker image for an egg, refer to the [Creating a Custom Docker Image](/v1/guides/egg-creation/creating-custom-image) guide for best practices on building and tagging images for Pterodactyl.
## Providing Multiple Images [#providing-multiple-images]
An egg can offer **multiple Docker images** for the server owner to choose from. To do this, list each image on a new line in the egg's Docker Image field (in the egg configuration). You can optionally prepend a display name to each image using the format:
`Display Name|docker/image:tag`
For example, you might offer two Java images for a Minecraft egg:
* `Java 17|ghcr.io/pterodactyl/yolks:java_17`
* `Java 21|ghcr.io/pterodactyl/yolks:java_21`
This would present the user with a dropdown choice between **"Java 17"** and **"Java 21"** when deploying the server. The text before the `|` is the friendly name shown in the panel, and the text after the `|` is the actual Docker image to use.
If you're not sure which image to use for your egg, start with one of the [Official Yolks](https://github.com/pterodactyl/yolks) that closely matches your server's requirements. These images are maintained by the community and cover most common use cases.
# Egg Install Script
## What is the Install Script? [#what-is-the-install-script]
The install script is where the **egg magic** happens.
When you click on the **Install Script** tab for the egg, you will see four main parts:
* A large text area to write the script. (Install Script)
* A field where you can select a different egg's script, default set to 'None' (Copy Script From)
* A field to select the **Docker image** in which the install script will run (Script Container).
* A field used to specify the entrypoint command used for the script, usually 'bash' (Script Entrypoint Command)
You can choose a special Docker image for the installation process (separate from the server's runtime image). Pterodactyl provides some "installer" images (Alpine or Debian based) that include common utilities like `curl`, `wget`, `unzip`, `git`, etc., to help with installation tasks. If you're not sure, use one of the official installer images:
* `ghcr.io/ptero-eggs/installers:alpine`
* `ghcr.io/ptero-eggs/installers:debian`
* `ghcr.io/ptero-eggs/installers:ubuntu`
You are not limited to these images — any Docker image compatible with Pterodactyl can be used as the install container. For example, some eggs use `eclipse-temurin:8-jdk-jammy` when the install process requires Java.
Then, in the script text area, write the shell commands needed to set up the server.
Anything you install or change **outside of `/mnt/server`** in the install container will **not persist** to the runtime server. The install script runs in a temporary environment. Only the `/mnt/server` directory (which corresponds to the server's file directory) is transferred to the actual server container. If your server needs specific system packages or libraries, those must be installed in the **runtime Docker image**, not just in the install script.
**How the install process works:**
1. **Create the server directory:** The script typically starts by making sure the `/mnt/server` directory exists and is the current working directory.
* For example, you might use:
```bash
mkdir -p /mnt/server
cd /mnt/server
```
* (In many official scripts, the container's working directory is already set to `/mnt/server`, but it's good practice to ensure it.)
2. **Download and prepare files:** Fetch any necessary files (server binaries, mods, configs, etc.) from the internet or local sources.
* Common tools for this are `curl`, `wget`, `git clone`, or using a command pipeline like `curl ... | tar ...`.
* For example, you might download a ZIP of the server software and then unzip it.
3. **Set up configurations:** If your server requires an initial configuration file or certain directory structure, you can create those here.
* For instance, you might generate a default config file, or rename files, etc.
* You can also use the values of variables by referencing environment variables (e.g. `${MAX_PLAYERS}` in the script will use the value from a custom egg variable if one exists).
4. **Install any additional dependencies (if needed for installation):** In some cases, the install process itself might require extra tools or packages. Since the official installer images come with common tools, this is rarely needed, but you could install others (e.g., `apk add ...` or `apt install ...`) in the install container. Remember, these tools won't be present in the runtime container, so this step is only for things needed *during installation* (not for actually running the server).
## Example Install Script [#example-install-script]
```bash
#!/bin/bash
# Create and navigate to the server directory
mkdir -p /mnt/server
cd /mnt/server || exit 1
# Install dependencies required for installation
apt update && apt install -y unzip wget
# Download necessary files
wget -O game-server.zip "https://example.com/game-server.zip"
unzip game-server.zip
rm game-server.zip
# Set up configuration
echo "max_players=32" > config.cfg
```
## What Happens After Installation? [#what-happens-after-installation]
When the install script finishes running:
* All files and folders **inside** `/mnt/server` (the server's directory) are retained and moved to the server's persistent storage.
* The install container is destroyed. Then the server's normal runtime container (using the Docker image specified in the egg's configuration) will start up, and it will have all the files that were in `/mnt/server` available.
In summary, the install script's job is to populate `/mnt/server` with everything the server needs. Once that's done, those files persist, and then the server can be launched in the proper environment.
# Egg Variables
One powerful feature of eggs is the ability to define **variables** that can be used to customize the server's startup and configuration without editing the startup command directly. Egg variables are exposed as environment variables to the server and can be referenced in the install script or config files.
To manage an egg's variables, create the egg (or edit an existing one) and navigate to the **Variables** tab on the egg configuration page.
## Types of Egg Variables [#types-of-egg-variables]
* **Default Variables:** These are provided by the Panel/Wings automatically for every server. You do **not** need to create these; they always exist.
* **Custom Variables:** These are defined by you on the egg's Variables tab.
## Default Variables [#default-variables]
These are injected into every server environment by default and can be referenced using the following syntaxes:
* In the startup command: `{{VARIABLE}}`
* In scripts: `$VARIABLE_NAME`
* In configuration parser entries: `{{env.VARIABLE}}`
| Variable Name | Description | Example |
| --------------------------- | ------------------------------------------------------------------- | -------------------------------------- |
| `TZ` | Time Zone set in the panel's `.env` file | `Etc/UTC` |
| `STARTUP` | The actual resolved startup command for the server | `./run.sh -arg1` |
| `SERVER_MEMORY` | Allocated memory for the server in megabytes | `1024` |
| `SERVER_IP` | The IP address assigned to the primary allocation | `192.168.1.2` |
| `SERVER_PORT` | The main port assigned to the server | `27015` |
| `P_SERVER_LOCATION` | The name of the location (set by the admin in the panel) | `Amsterdam-01` |
| `P_SERVER_UUID` | UUID of the server instance (used for tracking within Wings) | `ab12cd34-5678-90ef-ghij-klmn12345678` |
| `P_SERVER_ALLOCATION_LIMIT` | The maximum number of allocations available to this server (if set) | `3` |
| `USER` | The user executing processes inside the container | `container` |
| `HOME` | The home directory inside the container | `/home/container` |
## Custom Variables [#custom-variables]
Each variable allows you to define:
* The Environment Variable, for example: `MAX_PLAYERS`
* A default value
* Description (shown to the user in the panel)
* Validation Rules
* Whether it is viewable or editable by the user
## Creating a New Variable [#creating-a-new-variable]
When creating a new custom variable, you will provide:
* **Name:** A friendly name for the variable (e.g. "Max Players").
* **Description:** A description shown to the user (explain what the variable does).
* **Environment Variable:** The actual environment variable name used in code (use **UPPERCASE** letters, numbers, and underscores only). For example, `MAX_PLAYERS`. This is the name that will be referenced in the startup command or configuration files.
* **Default Value:** (Optional) A default value for this variable. This will be used if the user doesn't input anything else.
* **User Permissions:** Whether the user can view and/or edit this variable on their server:
* *Users Can View* — If set, the user can see this variable (and its value) in their server's settings.
* *Users Can Edit* — If set, the user can change the value of this variable from the default.
(If neither option is enabled, the variable is essentially hidden from the user's front-end view, though it still exists in the server's environment.)
### Validation Rules [#validation-rules]
**Rules:** Validation rules for the user's input. This uses Laravel's validation rule format. Common rules include:
* `required` — value must be provided
* `nullable` — value can be left empty
* `string` — must be a string
* `numeric` — must be a number
* `boolean` — must be true or false
* `between:1,10` — string length or numeric value between 1 and 10
* `max:64` — maximum string length or numeric value
* `in:value1,value2` — must be one of the listed values (e.g., `in:true,false`)
* `regex:/pattern/` — must match a regex pattern
Rules are combined with `|`. For example, `required|string|between:1,10` means the value is required, must be a string, and 1 to 10 characters in length. To require a value ending in ".jar", you could use `required|regex:/^([\w\d._-]+)(\.jar)$/`.
Even if you choose not to allow users to view or edit a variable, **be aware that it's not truly secret from the user**. Advanced users could still find the variable's value (since it exists in the server environment). Typically, hiding a variable is just to prevent casual users from changing or seeing it when it's not necessary for them to interact with (for example, a variable that is used internally by the egg).
After creating custom variables, both the custom and default variables can be seen when viewing the server's startup in the Panel (in both Admin and client views). The startup command preview will show these variables substituted with their actual values.
# Minecraft
## Configuring a Server Network (BungeeCord, Waterfall, HexaCord, etc.) [#configuring-a-server-network-bungeecord-waterfall-hexacord-etc]
If you want to operate Minecraft proxy servers like BungeeCord, Waterfall, HexaCord, etc. securely, you can do so with pterodactyl alone as long as you stay on the same node. It differs from a traditional setup in a few ways and might require additonal firewall rules, which is what this guide is for.
For the setup described below, it is necessary that all servers are on the same node.
If you are a hosting provider, you should only allow a single proxy network per node, if you are selling them to customers.
### Allocations in the Panel [#allocations-in-the-panel]
Create a regular allocation for the proxy server which uses the external IP of the node, so users can reach it.
The actual game servers behind the proxy should use allocations with `127.0.0.1` as the address, so they are only reachable on the node, and not from the public.
#### Example [#example]
`10.1.70.62` is an example, replace it with your own public IP address.
### proxy server settings [#proxy-server-settings]
As the proxy server, like all servers, is running in a docker container with network isolation, `localhost`/`127.0.0.1` doesn't refer to the node, but to the container. The node can be reached from within the container using `172.18.0.1` (unless the pterodactyl network is configured differently) instead. You therefore need to use this IP in your proxy server configuration.
#### bungeecord/waterfall configuration [#bungeecordwaterfall-configuration]
This will be different for other proxy servers, please refer to their documentation.
### paper/spigot/bukkit settings [#paperspigotbukkit-settings]
The servers itself require the regular config options required by server proxies, which usually comes down to disabling online mode. This will differ for other server software, please refer to their documentation.
#### server.properties [#serverproperties]
set online-mode `false`
#### spigot.yml [#spigotyml]
set bungeecord to `true`
### Firewalls [#firewalls]
If you are using a firewall, additional rules might be required to allow servers to reach each other on the node. In this case the proxy server needs to reach all of the game servers behind it. Therefore we need to allow traffic from the pterodactyl network to the server ports on localhost.
You can use the following commands as an example. `172.18.0.1` is the default address referring to the node within the pterodactyl network. Replace `` with the allocated localhost ports of the game servers.
The following commands will allow any server on the node to access the opened ports.
#### UFW (Ubuntu) [#ufw-ubuntu]
Allow access to the pterodactyl pterodactyl0 network on a specific port.
```bash
ufw allow in on pterodactyl0 to 172.18.0.1 port proto tcp
```
#### Firewalld (CentOS) [#firewalld-centos]
Allow access to pterodactyl0 from the pterodactyl0 network.
This command will allow any server to access all other servers as well as all ports on the node.
```bash
firewall-cmd --permanent --zone=public --add-source=172.18.0.1
```
# CentOS 7
This guide provides comprehensive instructions for installing Pterodactyl v1.X on CentOS 7, including all dependencies and SSL configuration.
## Install Dependencies [#install-dependencies]
### SELinux Configuration [#selinux-configuration]
If SELinux is enabled (check with `sestatus`), install the following packages:
```bash
yum install -y policycoreutils policycoreutils-python selinux-policy selinux-policy-targeted libselinux-utils setroubleshoot-server setools setools-console mcstrans
```
### Installing Dependencies [#installing-dependencies]
Run the following commands to install all necessary dependencies:
```bash
# Add MariaDB repository
sudo tee /etc/yum.repos.d/mariadb.repo <
You will need to change the fastcgi\_pass path in the Nginx configuration to `/var/run/php-fpm/pterodactyl.sock`
# Enterprise Linux 8 and Fedora Server 40
This guide provides comprehensive instructions for installing Pterodactyl v1.X on CentOS 8, Rocky Linux 8, AlmaLinux 8, and Fedora Server 40, including all dependencies.
## Install Dependencies [#install-dependencies]
### SELinux Configuration [#selinux-configuration]
If SELinux is enabled (check with `sestatus`), install the following packages:
```bash
sudo dnf install -y policycoreutils selinux-policy selinux-policy-targeted setroubleshoot-server setools setools-console mcstrans
```
### Installing Dependencies [#installing-dependencies]
Run the following commands to install all necessary dependencies:
```bash
# Update system
sudo dnf update -y
# Install EPEL and Remi repository
sudo dnf install -y epel-release
# Add additional repositories for PHP (Enterprise Linux 8)
sudo dnf install -y https://rpms.remirepo.net/enterprise/remi-release-8.rpm
# Add additional repositories for PHP (Fedora Server 40)
sudo dnf install -y https://rpms.remirepo.net/fedora/remi-release-40.rpm
# Enable PHP 8.3 from Remi
sudo dnf module reset php
sudo dnf module enable php:remi-8.3 -y
# Install dependencies
sudo dnf install -y php php-{common,cli,gd,mysql,mbstring,bcmath,xml,fpm,curl,zip} mariadb mariadb-server nginx redis zip unzip tar
# Start and enable services
sudo systemctl enable --now mariadb nginx redis
# Configure firewall
sudo firewall-cmd --add-service=http --permanent
sudo firewall-cmd --add-service=https --permanent
sudo firewall-cmd --reload
# Install Composer
curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer
```
## PHP Configuration [#php-configuration]
Create a new PHP-FPM configuration file in /etc/php-fpm.d/www-pterodactyl.conf:
```shell
[pterodactyl]
user = nginx
group = nginx
listen = /var/run/php-fpm/pterodactyl.sock
listen.owner = nginx
listen.group = nginx
listen.mode = 0750
pm = ondemand
pm.max_children = 9
pm.process_idle_timeout = 10s
pm.max_requests = 200
```
Start and enable PHP-FPM:
```bash
sudo systemctl enable --now php-fpm
```
## Installing the Panel [#installing-the-panel]
Excellent, we now have all of the required dependencies installed and configured. From here, follow the [official Panel installation documentation](/v1/panel/getting-started#download-files).
You will need to change the fastcgi\_pass path in the Nginx configuration to `/var/run/php-fpm/pterodactyl.sock`
# Debian 11, 12 & 13
This guide is based off the [official installation documentation](/v1/panel/getting-started) but is tailored specifically for Debian 11, 12 and 13.
| Operating System | Version | Supported | Notes |
| ---------------- | ------- | --------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Debian** | 11 | ✅ | |
| | 12 | ✅ | |
| | 13 | ✅ | - MariaDB can be installed without the repo setup script - Redis can be installed without the Redis APT repository |
## Dependency Installation [#dependency-installation]
In this guide, we will install the required dependencies for the Pterodactyl panel. After that, you can follow the official installation documentation.
```bash
# Install necessary packages
apt install -y curl ca-certificates gnupg2 sudo lsb-release
# Add additional repositories for PHP
echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/sury-php.list
curl -fsSL https://packages.sury.org/php/apt.gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/sury-keyring.gpg
# Add Redis official APT repository (Debian 11 & 12)
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
# MariaDB repo setup script (Debian 11 & 12)
curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | sudo bash
# Update repositories list
apt update
# Install Dependencies
apt install -y php8.3 php8.3-{common,cli,gd,mysql,mbstring,bcmath,xml,fpm,curl,zip} mariadb-server nginx tar unzip git redis-server
```
### Installing Composer [#installing-composer]
Composer is a dependency manager for PHP that allows us to ship everything you'll need code wise to operate the Panel. You'll need composer installed before continuing in this process.
```bash
curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer
```
### Download Files [#download-files]
Great, now all of the dependencies have been dealt with. Continue the installation by following the [official documentation Download Files section](/v1/panel/getting-started#download-files).
### Wings [#wings]
There is no additional configuration required for Wings on Debian 11, 12 or 13. You can follow the [official Wings install documentation](/v1/wings/installing), which covers Docker installation for Debian.
# Artisan CLI
The Artisan CLI, command line interface, is part of the Laravel framework, which Pterodactyl is built on. The Artisan file is located in `/var/www/pterodactyl` if you followed the official guide. This guide goes over some more Pterodactyl specific/related Artisan commands, which are all prefixed with the letter `p` (e.g. `p:user:make`). If you'd like to view all commands, you can do so by running:
```bash
php artisan list
```
To get information regarding a specific command you can do so by running:
```bash
php artisan help
```
To simplify this documentation, in command usage you'll see things like the following:
`` - Required argument
`[hello-world]` - Optional argument
`{--hello-world}` - Option
## User Management [#user-management]
When running any of the following commands, you can either use the options or don't pass through anything and use the interactive prompt. You can also do both passing through options and using interactive prompts as well.
### Create User [#create-user]
```bash
php artisan p:user:make {--email=user@example.com}
{--username=myusername}
{--name-first=My}
{--name-last=Name}
{--password=supersecret}
{--admin=1|0}
{--no-password}
```
### Delete User [#delete-user]
```bash
php artisan p:user:delete {--user=username/email/UUID}
```
### Disable 2FA [#disable-2fa]
Disabling 2-factor authentication should only be used as a last resort for user recovery. **Please use this with caution.**
```bash
php artisan p:user:disable2fa {--email=user@example.com}
```
## Server & Node Management [#server--node-management]
### Create Location [#create-location]
```bash
php artisan p:location:make {--short=us1}
{--long="A description of this location."}
```
### Delete Location [#delete-location]
```bash
php artisan p:location:delete {--short=us1}
```
### Server Bulk Power [#server-bulk-power]
```bash
php artisan p:server:bulk-power
{--servers=1,2,3}
{--nodes=1,2,3}
```
## Panel Management [#panel-management]
### View Panel Info [#view-panel-info]
```bash
php artisan p:info
```
Displays a variety of panel information that can be used to check the configuration of things such as database and email.
### Update Panel [#update-panel]
```bash
php artisan p:upgrade {--user=www-data}
{--group=www-data}
{--url=https://example.com/panel.tar.gz}
{--release=latest}
{--skip-download}
```
Downloads a new archive for Pterodactyl and executes the normal upgrade commands.
# Setting up MySQL
## Creating a database for Pterodactyl [#creating-a-database-for-pterodactyl]
MySQL is a core component of Pterodactyl Panel but it can be confusing to setup and use if you've never done so before. This is a very basic tutorial that skims just enough of the surface to set MySQL up and running with the panel. If you're interested in learning more, there are some great tutorials available on the Internet.
### Logging In [#logging-in]
The first step in this process is to login to the MySQL command line where we will be executing some statements to get things setup. To do so, simply run the command below and provide the Root MySQL account's password that you setup when installing MySQL. If you do not remember doing this, chances are you can just hit enter as no password is set.
```sql
# If using MariaDB (v11.0.0+)
mariadb -u root -p
# If using MySQL
mysql -u root -p
```
### Creating a user [#creating-a-user]
For security sake, and due to changes in MySQL 5.7, you'll need to create a new user for the panel. To do so, we want to first tell MySQL to use the mysql database, which stores such information.
Next, we will create a user called `pterodactyl` and allow logins from localhost which prevents any external connections to our database. You can also use `%` as a wildcard or enter a numeric IP. We will also set the account password to `somePassword`.
```sql
# Remember to change 'somePassword' below to be a unique password specific to this account.
CREATE USER 'pterodactyl'@'127.0.0.1' IDENTIFIED BY 'somePassword';
```
### Create a database [#create-a-database]
Next, we need to create a database for the panel. In this tutorial we will be naming the database `panel`, but you can substitute that for whatever name you wish.
```sql
CREATE DATABASE panel;
```
### Assigning permissions [#assigning-permissions]
Finally, we need to tell MySQL that our pterodactyl user should have access to the panel database. To do this, simply run the command below.
```sql
GRANT ALL PRIVILEGES ON panel.* TO 'pterodactyl'@'127.0.0.1';
```
## Creating a Database Host for Nodes [#creating-a-database-host-for-nodes]
This section covers creating a MySQL user that has permission to create and modify users. This allows the Panel to create per-server databases on the given host.
### Creating a user [#creating-a-user-1]
If your database is on a different host than the one where your Panel or Daemon is installed make sure to use the IP address of the machine the Panel is running on. If you use `127.0.0.1` and try to connect externally, you will receive a connection refused error.
```sql
# You should change the username and password below to something unique.
CREATE USER 'pterodactyluser'@'127.0.0.1' IDENTIFIED BY 'somepassword';
```
### Assigning permissions [#assigning-permissions-1]
The command below will give your newly created user the ability to create additional users, as well as create and destroy databases. As above, ensure `127.0.0.1` matches the IP address you used in the previous command.
```sql
GRANT ALL PRIVILEGES ON *.* TO 'pterodactyluser'@'127.0.0.1' WITH GRANT OPTION;
```
### Allowing external database access [#allowing-external-database-access]
Chances are you'll need to allow external access to this MySQL instance in order to allow servers to connect to it. To do this, open `my.cnf`, which varies in location depending on your OS and how MySQL was installed. You can type `find /etc -iname my.cnf` to locate it.
Open `my.cnf`, add text below to the bottom of the file and save it:
```
[mysqld]
bind-address=0.0.0.0
```
Restart MySQL/MariaDB to apply these changes. This will override the default MySQL configuration, which by default will only accept requests from localhost. Updating this will allow connections on all interfaces, and thus, external connections. Make sure to allow the MySQL port (default 3306) in your firewall.
If your Database and Wings are on the same machine and won't need external access, you can also use the `docker0` interface IP address rather than `127.0.0.1`. This IP address can be found by running `ip addr | grep docker0`, and it likely looks like `172.x.x.x`.
Starting with MySQL 8.0.13 / MariaDB 10.11 or above, `bind_address` now also accepts a comma-separated list of interfaces to give more control over what interfaces it will listen on and which not.
# Creating SSL Certificates
This tutorial briefly covers creating new SSL certificates for your panel and wings.
To begin, we will install certbot, a simple script that automatically renews our certificates and allows much easier creation of them. The command below is for Ubuntu distributions, but you can always check [Certbot's official site](https://certbot.eff.org/) for installation instructions. We have also included a command below to install certbot's Nginx/Apache plugin so you won't have to stop your webserver.
```bash
sudo apt update
sudo apt install -y certbot
# Run this if you use Nginx
sudo apt install -y python3-certbot-nginx
# Run this if you use Apache
sudo apt install -y python3-certbot-apache
```
## Creating a Certificate [#creating-a-certificate]
After installing the certbot, we need to generate a certificate. There are a couple of ways to do that, but the easiest is to use the web server-specific certbot plugin you just installed. For Wings-only machines that don't need a web server, use the standalone or DNS method of the certbot as you don't need a web server for it.
Then, in the command below, you should replace `example.com` with the domain you would like to generate a certificate for. When you have multiple domains you would like certificates for, simply add more `-d anotherdomain.com` flags to the command. You can also look into generating a wildcard certificate but that is not covered in this tutorial.
When you are using certbot's Nginx/Apache plugin, you won't need to restart your webserver to have the certificate applied assuming that you've already configured the webservers to use SSL as instructed in the [web server configuration step](/v1/panel/webserver-configuration).
### HTTP challenge [#http-challenge]
HTTP challenge requires you to expose port 80 for the challenge verification.
```bash
# Nginx
certbot certonly --nginx -d example.com
# Apache
certbot certonly --apache -d example.com
# Standalone - Use this if neither works. Make sure to stop your webserver first when using this method.
certbot certonly --standalone -d example.com
```
### DNS challenge [#dns-challenge]
DNS challenge requires you to create a new TXT DNS record to verify domain ownership, instead of having to expose port 80. The instructions are displayed when you run the certbot command below.
```bash
certbot -d example.com --manual --preferred-challenges dns certonly
```
### Auto Renewal [#auto-renewal]
You'll also probably want to configure the automatic renewal of certificates to prevent unexpected certificate expirations. You can open crontab with `sudo crontab -e` and add the line from below to the bottom of it for attempting renewal every day at 23 (11 PM).
Deploy hook would restart the Nginx service to apply a new certificate when it's renewed successfully. Change `nginx` in the restart command to suit your own needs, such as to `apache` or `wings`.
For advanced users, we suggest installing and using [acme.sh](https://acme.sh) which provides more options, and is much more powerful than certbot.
```text
0 23 * * * certbot renew --quiet --deploy-hook "systemctl restart nginx"
```
### Troubleshooting [#troubleshooting]
If you get an `Insecure Connection` or SSL/TLS related error when trying to access your panel or wings, the certificate has likely expired. This can be easily fixed by renewing the SSL certificate, although using the command `certbot renew` might not do the job if port 80 is in use, as it'll return errors like: `Error: Attempting to renew cert (domain) from /etc/letsencrypt/renew/domain.conf produced an unexpected error`.
This will happen especially if you're running Nginx instead of Apache. The solution for this is to use Nginx or Apache plugins with `--nginx` and `--apache`. Alternatively, you can stop Nginx, then renew the certificate, and finally restart Nginx. Replace `nginx` with your own web server or with `wings` should you be renewing the certificate for Wings.
Stop Nginx:
```bash
systemctl stop nginx
```
Renew the certificate:
```bash
certbot renew
```
Once the process has completed, you can restart the Nginx service:
```bash
systemctl start nginx
```
You may also need to restart Wings as not every service is able to automatically apply an updated certificate:
```bash
systemctl restart wings
```
This is for advanced users, whose server systems do not have access to port 80. The command below is for Ubuntu distributions and CloudFlare API (you may google for other APIs for other DNS providers), but you can always check [acme.sh's official site](https://github.com/acmesh-official/acme.sh) for installation instructions. Make sure you read both instructions, as some people may have moved to CloudFlare's [new authorization system](https://blog.cloudflare.com/permissions-best-practices) (Modern), but others [have not](https://cloudflare.tv/event/ea8JJLgR) (Legacy).
```bash
curl https://get.acme.sh | sh
```
### Obtaining CloudFlare API Key (Legacy) [#obtaining-cloudflare-api-key-legacy]
After installing acme.sh, we need to fetch a CloudFlare API key. On Cloudfare's website, select your domain, then on the right side, copy your "Zone ID" and "Account ID" then click on "Get your API token", click on "Create Token" > select the template "Edit zone DNS" > select the scope of "Zone Resources" and then click on "Continue to summary", copy your token.
### Creating a Certificate [#creating-a-certificate-1]
Since the configuration file is based on Certbot, we need to create the folder manually.
```bash
sudo mkdir -p /etc/letsencrypt/live/example.com
```
After installing acme.sh and obtaining CloudFlare API key, we need to then generate a certificate. First, input the CloudFlare API credentials.
```bash
export CF_Token="Your_CloudFlare_API_Key"
export CF_Account_ID="Your_CloudFlare_Account_ID"
export CF_Zone_ID="Your_CloudFlare_Zone_ID"
```
### Obtaining CloudFlare API Key (Modern) [#obtaining-cloudflare-api-key-modern]
After installing acme.sh, we need to fetch a CloudFlare API key. On Cloudfare's website, click on your profile on the top right. Then go to "My Profile", on the left you will find "API Tokens". Click it and it'll bring you to [the API tokens page](https://dash.cloudflare.com/profile/api-tokens). Select "Create Token" and use the "Edit zone DNS" template. Then once on the next page, go to "Zone Resources" and "Include" - "Specific Zone" - (Select the domain you want to use). Then continue to the summary. Confirm you'd like to create the token.
### Creating a Certificate [#creating-a-certificate-2]
Since the configuration file is based on Certbot, we need to create the folder manually.
```bash
sudo mkdir -p /etc/letsencrypt/live/example.com
```
After installing acme.sh and obtaining the CloudFlare API key, we need to then generate a certificate. First, input the CloudFlare API credentials.
```bash
export CF_Key="Your_CloudFlare_API_Key"
export CF_Email="Your_CloudFlare_Email"
```
Then create the certificate. Since the API key is bound to the domain, Cloudflare should allow you to generate one.
```bash
acme.sh --issue --dns dns_cf -d "example.com" --server letsencrypt \
--key-file /etc/letsencrypt/live/example.com/privkey.pem \
--fullchain-file /etc/letsencrypt/live/example.com/fullchain.pem
```
### Auto Renewal [#auto-renewal-1]
After running the script for the first time, it will be added to the crontab automatically. You may edit the auto-renewal interval by editing the crontab.
```bash
sudo crontab -e
```
This is for advanced users, who are running Cloudflare in proxy mode or do not have access to port `80`.
### Installing Caddy with Cloudflare DNS plugin [#installing-caddy-with-cloudflare-dns-plugin]
Caddy does not come by default with Cloudflare DNS plugin, you need to install it yourself.
There are two main methods:
1. Using `xcaddy` - CLI tool to build your own Caddy build
2. Downloading prebuilt binary from [Caddy's download page](https://caddyserver.com/download).
3. Using Ansible to download and install Caddy with plugins. See [caddy-ansible](https://github.com/caddy-ansible/caddy-ansible)
#### Build Caddy using `xcaddy` on your server [#build-caddy-using-xcaddy-on-your-server]
Please refer to [Caddy docs on building Caddy](https://caddyserver.com/docs/build#xcaddy).
### Obtaining CloudFlare API Token [#obtaining-cloudflare-api-token]
After installing acme.sh, we need to fetch a CloudFlare API key. Please make sure that a DNS record (A or CNAME record) is pointing to your target node, and set the cloud to grey (bypassing CloudFlare proxy). Then go to My Profile > API keys and on Global API Key subtab, click on "view", enter your CloudFlare password, and copy the API key to clipboard.
After install Caddy with Cloudflare DNS plugin, we need to fetch a Cloudflare API token. Please make sure that a DNS record (A or CNAME record) is pointing at your target node. Then go to My Profile > API Tokens and on API Tokens click "Create Token". Create API Token > API token templates, at the end of line with "Edit zone DNS", click "Use template". Under **Zone Resources**, select your DNS zone for which you wish to create the API token, click "Continue to summary". Review the API token summary and click "Create Token". And finally copy the API token to clipboard.
### Reconfiguring Caddy to use Cloudflare DNS for obtaining certificates [#reconfiguring-caddy-to-use-cloudflare-dns-for-obtaining-certificates]
Create an environment variable file (like `.env`), keep in mind that this file contains secrets and should not be accessed by public.
We recommend that you create the secret file in the following location: `/etc/caddy/.secrets.env`.
```bash
CLOUDFLARE_API_TOKEN=
```
For security reasons, we recommend setting permissions to `0600` (only owner can read or write to the file).
```bash
# Set ownership of the `.secrets.env` file to `caddy` system user
chown caddy:caddy /etc/caddy/.secrets.env
# Set read-write permissions only to owner - the `caddy` system user
chmod 0600 /etc/caddy/.secrets.env
```
Modify the systemd unit file, to load environment variables from file (add `--envfile /etc/caddy/.secrets.env` flag to `ExecStart`), the default systemd unit file location is `/etc/systemd/system/caddy.service`:
```shell {12}
[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target
[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --environ --envfile /etc/caddy/.secrets.env --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
```
You can add a `tls` block to your `Caddyfile`, under the `` block of your panel configuration, the Caddy config file location is `/etc/caddy/Caddyfile`:
```shell {5-7}
{
# ...
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
}
```
# CentOS 7
This guide provides comprehensive instructions for installing Pterodactyl Wings v1.X on CentOS 7.
## Install Dependencies [#install-dependencies]
```bash
## Install yum tools
yum install -y yum-utils device-mapper-persistent-data lvm2
## Add the docker repo
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
## Install docker
yum install -y docker-ce docker-ce-cli
## Enable docker service
systemctl enable --now docker
# Configure firewall
firewall-cmd --add-port 8080/tcp --permanent
firewall-cmd --add-port 2022/tcp --permanent
firewall-cmd --permanent --zone=trusted --change-interface=docker0
firewall-cmd --zone=trusted --add-masquerade --permanent
firewall-cmd --reload
```
## Installing Wings [#installing-wings]
Great, now all of the dependencies and firewall rules have been dealt with. From here follow the [official Wings installation documentation](/v1/wings/installing#enabling-swap).
# Enterprise Linux 8 and Fedora Server 40
This guide provides comprehensive instructions for installing Pterodactyl Wings v1.X on CentOS 8, Rocky Linux 8, AlmaLinux 8 and Fedora Server 40.
## Install Dependencies [#install-dependencies]
```bash
# Install required packages
sudo dnf install -y dnf-utils device-mapper-persistent-data lvm2
# Add Docker repository (Enterprise Linux 8)
sudo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
# Add Docker repository (Fedora Server 40)
sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
## Install Docker
sudo dnf install -y docker-ce docker-ce-cli containerd.io
## Enable Docker service
systemctl enable --now docker
# Configure firewall
firewall-cmd --add-port 8080/tcp --permanent
firewall-cmd --add-port 2022/tcp --permanent
firewall-cmd --permanent --zone=trusted --change-interface=pterodactyl0
firewall-cmd --zone=trusted --add-masquerade --permanent
firewall-cmd --reload
```
## Installing Wings [#installing-wings]
Great, now all of the dependencies and firewall rules have been dealt with. From here follow the [official Wings installation documentation](/v1/wings/installing#enabling-swap).
If you have SELinux enforcement enabled and you are getting AVC denials from your containers, try relocating your Wings data directory from `/var/lib/pterodactyl` to `/var/srv/containers/pterodactyl`. That is where the targeted policy expects Docker to read and write data from.
# Delete location
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Deletes the specified location
# List locations
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves all locations
# Available include parameters [#available-include-parameters]
| Parameter | Description |
| --------- | -------------------------------------- |
| nodes | List of nodes assigned to the location |
| servers | List of servers in the location |
# Location details
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves the specified location
# Available include parameters [#available-include-parameters]
| Parameter | Description |
| --------- | -------------------------------------- |
| nodes | List of nodes assigned to the location |
| servers | List of servers in the location |
# Update location
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates the specified location
# Create location
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a new location
# List nests
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves all nests
# Available include parameters [#available-include-parameters]
| Parameter | Description |
| --------- | ------------------------------- |
| eggs | List of eggs in the location |
| servers | List of servers in the location |
# Nest details
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves the specified nests
# Available include parameters [#available-include-parameters]
| Parameter | Description |
| --------- | ------------------------------- |
| eggs | List of eggs in the location |
| servers | List of servers in the location |
# Egg details
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves the specified egg
## Available include parameters [#available-include-parameters]
| Parameter | Description |
| --------- | -------------------------------------------- |
| nest | Information about the nest that owns the egg |
| servers | List of servers using the egg |
| config | Config options of the egg |
| script | Egg install script |
| variables | List of egg variables |
# List eggs
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves a list of eggs
## Available include parameters [#available-include-parameters]
| Parameter | Description |
| --------- | -------------------------------------------- |
| nest | Information about the nest that owns the egg |
| servers | List of servers using the egg |
| config | Config options of the egg |
| script | Egg install script |
| variables | List of egg variables |
# Delete node
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Deletes the specified node
# List deployable nodes
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns nodes that have enough resources for a new server with the given requirements
# List nodes
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves a list of nodes
## Available include parameters [#available-include-parameters]
| Parameter | Description |
| ----------- | ------------------------------------------------------ |
| allocations | List of allocations added to the node |
| location | Information about the location the node is assigned to |
| servers | List of servers on the node |
# Node configuration
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Displays the Wings configuration
# Node details
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves the specified node
## Available include parameters [#available-include-parameters]
| Parameter | Description |
| ----------- | ------------------------------------------------------ |
| allocations | List of allocations added to the node |
| location | Information about the location the node is assigned to |
| servers | List of servers on the node |
# Update node
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates the node details
# Create node
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a new node
# Delete allocation
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Deletes the specified allocation
# List allocations
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Lists allocations added to the node
## Available include parameters [#available-include-parameters]
| Parameter | Description |
| --------- | ------------------------------------------------------ |
| node | Information about the node the allocation belongs to |
| server | Information about the server the allocation belongs to |
# Create allocations
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Adds an allocation to the node
# Delete server
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Deletes the specified server
# Force delete server
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Forcefully deletes the specified server
# List servers
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves all servers
## Available include parameters [#available-include-parameters]
| Parameter | Description |
| ----------- | ------------------------------------------ |
| allocations | List of allocations assigned to the server |
| user | Information about the server owner |
| subusers | List of users added to the server |
| nest | Information about the server's egg nest |
| egg | Information about the server's egg |
| variables | List of server variables |
| location | Information about server's node location |
| node | Information about the server's node |
| databases | List of databases on the server |
# Server details
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves the specified server
## Available include parameters [#available-include-parameters]
| Parameter | Description |
| ----------- | ------------------------------------------ |
| allocations | List of allocations assigned to the server |
| user | Information about the server owner |
| subusers | List of users added to the server |
| nest | Information about the server's egg nest |
| egg | Information about the server's egg |
| variables | List of server variables |
| location | Information about server's node location |
| node | Information about the server's node |
| databases | List of databases on the server |
# Server details by external ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves a server by its external ID
## Available include parameters [#available-include-parameters]
| Parameter | Description |
| ----------- | ------------------------------------------ |
| allocations | List of allocations assigned to the server |
| user | Information about the server owner |
| subusers | List of users added to the server |
| nest | Information about the server's egg nest |
| egg | Information about the server's egg |
| variables | List of server variables |
| location | Information about server's node location |
| node | Information about the server's node |
| databases | List of databases on the server |
# Update build
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates the server build information
# Update details
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates the server details
# Update startup
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates the server startup information
# Create server
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a new server
# Reinstall server
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Reinstalls the specified server
# Suspend server
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Suspends the specified server
# Unsuspend server
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Unuspends the specified
# Delete database
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Deletes the specified database
# Database details
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves the specified database
## Available include parameters [#available-include-parameters]
| Parameter | Description |
| --------- | ----------------------------------- |
| password | Includes the database user password |
| host | Information about the database host |
# List databases
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves all databases on a server
## Available include parameters [#available-include-parameters]
| Parameter | Description |
| --------- | ----------------------------------- |
| password | Includes the database user password |
| host | Information about the database host |
# Create database
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a new database on the specified server
# Reset password
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Rotates the password of the database
# Delete user
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Deletes the specified user
# List users
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves all users
## Available Include parameters [#available-include-parameters]
| Parameter | Description |
| --------- | -------------------------------------- |
| servers | List of servers the user has access to |
## Filters [#filters]
| Parameter |
| ------------ |
| email |
| uuid |
| username |
| external\_id |
## Sort by [#sort-by]
| Parameter |
| --------- |
| id |
| uuid |
# User details
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves the specified user
## Available Include parameters [#available-include-parameters]
| Parameter | Description |
| --------- | -------------------------------------- |
| servers | List of servers the user has access to |
# User details by external ID
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves the specified user by its external ID
## Available Include parameters [#available-include-parameters]
| Parameter | Description |
| --------- | -------------------------------------- |
| servers | List of servers the user has access to |
# Update user
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates the user information
# Create user
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a new user
# Delete API key
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Deletes the specified API key
# 2FA details
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Generates a TOTP QR code image to allow the setup of 2FA
# Account activity
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns the activity log for the authenticated user's account
## Include parameters [#include-parameters]
| Parameter | Description |
| --------- | --------------------------------------------------- |
| actor | Information about the user who performed the action |
# Account details
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves information about the account
# List API keys
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retries a list of API keys
# List SSH keys
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns all SSH keys on the authenticated user's account
# Create API key
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Generates a new API key
# Create SSH key
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Adds an SSH key to the user's account. Used for SFTP authentication. Requires a 2048+ bit RSA key or an ECDSA/Ed25519 key.
# Disable 2FA
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Disables two-factor authentication on the account
# Enable 2FA
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Enables TOTP 2FA using the QR code generated by the GET request
Uses code generated from `GET /account/two-factor`
# Remove SSH key
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Removes an SSH key from the user's account by its fingerprint
# Update email
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates the email address of the account
# Update password
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates the password of the account
# List servers
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Lists all servers
## Include parameters [#include-parameters]
| Parameter | Description |
| --------- | ----------------------------------------- |
| egg | Information about the egg the server uses |
| subusers | List of subusers on the server |
# Show permissions
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retries all available permissions
This is used for the frontend
# Console details
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Generates credentials to establish a websocket
## How to connect [#how-to-connect]
1. Connect to the websocket address (in this example "wss\://pterodactyl.file.properties:8080/api/servers/1a7ce997-259b-452e-8b4e-cecc464142ca/ws")
2. Send the token to the websocket like this: `{"event":"auth","args":["eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6Ij..."]}`
* Tokens last about 10-15 minutes, and the websocket will notify you once you need to send a new token with `{"event":"token expiring"}` and `{"event":"token expired"}`
## Things you can send [#things-you-can-send]
* `{"event":"auth","args":[""]}` # Authenticate with websocket
* `{"event":"send stats","args":[null]}` # Request stats
* `{"event":"send logs","args":[null]}` # Request logs
* `{"event":"set state","args":[""]}` # Send power action
* `{"event":"send command","args":[""]}` # Send command
## Things you'll receive [#things-youll-receive]
* `{"event":"auth success"}` # Upon successful websocket authentication
* `{"event":"status","args":["offline"]}` # Status updates of the server
* `{"event":"console output","args":["[14:07:12] [Query Listener #1/INFO]: Query running on 0.0.0.0:25565"]}` # Logs from server
* `{"event":"stats","args":["{\"memory_bytes\":526626816,\"memory_limit_bytes\":588800000,\"cpu_absolute\":588.815,\"network\":{\"rx_bytes\":1126,\"tx_bytes\":1126},\"state\":\"stopping\",\"disk_bytes\":128118626}"]}` # Stats from server
* `{"event":"token expiring"}` # Token is expiring soon so request a new one and send it to the websocket
* `{"event":"token expired"}` # Token has expired
# Resource usage
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves resource utilization of the specified server
# Server activity
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns the activity log for the specified server
## Include parameters [#include-parameters]
| Parameter | Description |
| --------- | --------------------------------------------------- |
| actor | Information about the user who performed the action |
# Server details
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves information about the specified server
## Include parameters [#include-parameters]
| Parameter | Description |
| --------- | ----------------------------------------- |
| egg | Information about the egg the server uses |
| subusers | List of subusers on the server |
# Change power state
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Sends a power signal to the server
## Signals [#signals]
| Signal | Description |
| ------- | -------------------------------- |
| start | Starts the server |
| stop | Gracefully stops the server |
| restart | Stops then starts the server |
| kill | Instantly end the server process |
# Send command
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Sends a command to the server
The server must be online to send a command to it. You will get HTTP 502 is the server if not online.
# Delete backup
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Deletes the specified backup
# Backup details
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves information about the specified backup
# Download backup
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Generates a download link for a backup
# List backups
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves a list of backups
# Create backup
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a new backup
# Restore backup
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Restores a backup to the server. Existing files will be overwritten. Returns 400 if the backup has not completed, or if the server is currently installing, transferring, or restoring a different backup.
# Toggle backup lock
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Toggles the locked status of a backup. A locked backup cannot be deleted by the automatic backup rotation.
# Delete database
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Deletes the specified database
# List databases
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Lists all databases on a server
## Include parameters [#include-parameters]
| Parameter | Description |
| --------- | ----------------------------------- |
| password | Includes the database user password |
# Create database
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a new database
# Rotate password
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Changes the password of a specified database
# Download file
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Generates a one-time link to download the specified file
## Available parameters [#available-parameters]
| Parameter | Description |
| --------- | ------------------------------------ |
| file | URL encoded path to the desired file |
# Get file contents
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Displays the contents of the specified file
## Available parameters [#available-parameters]
| Parameter | Description |
| --------- | ------------------------------------ |
| file | URL encoded path to the desired file |
# List files
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Lists all files of the server
## Available parameters [#available-parameters]
| Parameter | Description |
| --------- | ----------------------------------- |
| directory | URL encoded path to list files from |
# Upload file
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns a signed URL used to upload files to the server using POST
# Change file permissions
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates file permissions for one or more files in the given directory
# Compress file
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Compresses the specified file
# Copy file
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Copies the specified file
# Create folder
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates the specified folder in the specified directory
# Decompress file
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Decompresses the selected file
# Delete file
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Deletes the specified file(s) or folder(s)
# Pull remote file
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Downloads a file from an external URL to the server
# Write file
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Writes data to the specified file
## Available parameters [#available-parameters]
| Parameter | Description |
| --------- | ------------------------------------ |
| file | URL encoded path to the desired file |
# Rename file
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Renames the specified file(s) or folder(s)
# Unassign allocation
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Deletes the specified non-primary allocation
# List allocations
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves the network information for the specified server
# Assign allocation
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Automatically assigns a new allocation if auto-assign is enabled on the instance
# Set allocation note
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Sets a note for the allocation
# Fields [#fields]
| Name | Required? | Type | Description | Rules |
| ----- | --------- | ------ | ------------------- | ----- |
| notes | required | string | Note for allocation | |
# Set primary allocation
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Sets the primary allocation
# Delete schedule
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Deletes the specified schedule
# Delete task
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Deletes the specified task
# List schedules
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Lists all schedules added to the server
# Schedule details
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves specific schedule
# Create schedule
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a new schedule
# Create task
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a new task on the specified schedule
# Execute schedule
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggers a schedule to run now, regardless of its cron timing
# Update schedule
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates the specified schedule
# Update task
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates the specified task
# Reinstall server
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Renames the server
# Rename server
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Renames the server
# Fields [#fields]
| Name | Required? | Type | Description | Rules |
| ---- | --------- | ------ | ----------------------- | ----- |
| name | required | string | New name for the server | |
# Update Docker image
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Changes the Docker image the server runs with. The image must be one of the images allowed by the server's egg. Returns 400 if the image was manually set by an admin.
# List Variables
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Lists all variables on the server
# Update Variable
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates the specified variable
# Delete user
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Removes the specified user from the server
# List Users
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Lists all users added to the server, along with details about them and their permissions
# User details
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves information about a specific user
# Create User
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Adds a user to the server
# Fields [#fields]
| Name | Required? | Type | Description | Rules |
| ----------- | --------- | ------ | ---------------------------------- | ----- |
| email | required | string | Email address of the user | |
| permissions | required | object | Permissions that user is permitted | |
# Update user
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates the specified user
# Fields [#fields]
| Name | Required? | Type | Description | Rules |
| ----------- | --------- | ------ | ---------------------------------- | ----- |
| permissions | required | object | Permissions that user is permitted | |
# Creating a Node
## Introduction [#introduction]
A node is a machine that runs Wings and your game servers. This guide covers each setting on the **Create Node** page. To install Wings, see [Installing Wings](../../wings/installing.mdx).
Every node belongs to a location, which groups nodes, for example by region or data center. To create one, open **Admin → Locations**, click **New Location**, and enter a **Short Code**, such as `us1`, and a **Description**.
## Creating the Node [#creating-the-node]
Open **Admin → Nodes** and click **New node**. The page has four sections.
### Identity [#identity]
* **Name:** A short name. It may contain letters, numbers, spaces, periods, underscores, and dashes.
* **Description:** Optional notes.
* **Location:** The location the node belongs to.
### Network [#network]
* **FQDN:** The domain name that points to the node, such as `node1.example.com`. Without SSL, you may use an IP address instead. With SSL, use a domain name, because certificates are issued for domain names.
* **Communicate Over SSL:** Whether the Panel talks to Wings over HTTPS. If your Panel uses HTTPS, the node must too, because browsers block insecure connections from a secure page.
* **Node Visibility:** Public nodes can receive servers the Panel places automatically. Private nodes only get servers you place on them.
* **Behind Proxy:** Turn on if a proxy, such as Cloudflare, handles SSL for Wings. Wings then does not look for its own certificate.
* **Maintenance Mode:** Stops users from managing servers on this node, for example while you work on it.
### Resource Limits [#resource-limits]
* **Memory (MiB)** and **Disk Space (MiB):** How much memory and disk space the Panel may give to servers on this node. Leave enough for the operating system and other software.
* **Memory Overallocate (%)** and **Disk Overallocate (%):** How far the Panel may exceed those totals. For example, with 10240 MiB of memory and 20% overallocation, the Panel may assign up to 12288 MiB. Use `0` to stay within the totals.
The Panel checks these limits when it places a server automatically or transfers one to this node. It does not stop you from creating a server on the node yourself.
### Daemon [#daemon]
* **Daemon Port:** The port Wings' API listens on. The default is `8080`.
* **Daemon SFTP Port:** The port Wings' SFTP server listens on. The default is `2022`.
* **Upload Size Limit (MiB):** The largest file users may upload through the file manager.
Click **Create Node**. The Panel opens the node's **Allocation** tab.
## Adding Allocations [#adding-allocations]
An allocation is an IP address and port that a server can use. Every server needs at least one. On the **Allocation** tab, fill in **Assign New Allocations**:
* **IP Address:** The IP address of the node's network interface. Behind NAT, use the internal address.
* **IP Alias:** Optional. A name or address, such as a domain name, shown to users instead of the IP address.
* **Ports:** One or more ports or port ranges, separated by commas or spaces, such as `25565, 25570-25580`.
Ports must be between 1025 and 65535, and a single range may contain at most 1000 ports. Click **Submit** to add them.
## Connecting Wings [#connecting-wings]
The node's **Configuration** tab shows its configuration file and an **Auto-Deploy** section, where **Generate Token** gives you a command that configures Wings. See [Configuring Wings](../../wings/installing.mdx#configuring-wings) for both methods.
Once Wings is running, the node's **About** tab shows its Wings version and system details.
# Using Mounts
## Introduction [#introduction]
A mount makes a directory on the node available inside a server's container, for example to share a large map, a mod pack, or plugins between servers without copying them into each one.
A mount has a **source**, the directory on the node, and a **target**, the path where it appears inside the container.
Servers only share a mount's files when they run on the same node. Mounts are not copied between nodes.
## Allowing the Directory in Wings [#allowing-the-directory-in-wings]
For security, Wings only mounts directories listed under `allowed_mounts` in `/etc/pterodactyl/config.yml`:
```yaml
allowed_mounts:
- /var/lib/pterodactyl/mounts
```
Add each mount's source directory to this list yourself, then restart Wings:
```bash
systemctl restart wings
```
A directory in `allowed_mounts` also allows every directory inside it.
## Creating a Mount [#creating-a-mount]
Open **Admin → Mounts**, click **New Mount**, and fill in:
* **Name:** A unique name.
* **Description:** Optional notes.
* **Source:** The absolute path of the directory on the node, such as `/var/lib/pterodactyl/mounts/maps`. Create the directory on each node you attach the mount to.
* **Target:** The absolute path inside the container, such as `/home/container/maps`. It cannot be `/home/container` itself.
* **Read Only:** Whether servers can only read the mount's files.
* **User Mountable:** Not used yet. Users cannot add mounts to their own servers; only administrators can attach mounts.
Click **Create Mount**. On the mount's page, use **Attach Eggs** and **Attach Nodes** to choose where it may be used. A server can only use a mount attached to both its egg and its node.
## Attaching a Mount to a Server [#attaching-a-mount-to-a-server]
1. Open the server in **Admin → Servers** and select its **Mounts** tab. The tab appears once the server is installed.
2. Click **Mount** next to the mount.
3. Restart the server.
The mount's files appear at the target path when the server starts. To remove the mount, click **Unmount** and restart the server.
Mounted files do not appear in the Panel's file manager or over SFTP, even when the target is inside `/home/container`. Only the server's own processes can see them.
# Upgrading PHP
## Introduction [#introduction]
Pterodactyl 2.0 needs PHP 8.3 or 8.4. If you run an older version, such as PHP 8.1 or 8.2 from Pterodactyl 1.x, upgrade PHP before you upgrade the Panel.
| Panel Version | PHP Versions |
| -------------- | ------------ |
| 2.x | 8.3, 8.4 |
| 1.11.10 to 1.x | 8.2, 8.3 |
This guide upgrades Ubuntu or Debian to PHP 8.4. For PHP 8.3, replace `8.4` with `8.3` in every command.
## Installing the New Version [#installing-the-new-version]
Add the `ondrej/php` repository, which provides newer PHP versions for Ubuntu:
```bash
apt -y install software-properties-common
LC_ALL=C.UTF-8 add-apt-repository -y ppa:ondrej/php
```
On Debian, skip this step. Sury's repository, added in [Getting Started](../../panel/getting-started.mdx#installing-dependencies), already provides newer PHP versions.
Install the new version with the extensions the Panel needs:
```bash
apt update
apt -y install php8.4 php8.4-{common,cli,gd,mysql,mbstring,bcmath,xml,fpm,curl,zip,intl}
```
The new version becomes the default `php` command. Check it with:
```bash
php -v
```
## Updating the Web Server [#updating-the-web-server]
The web server still sends requests to the old version's PHP-FPM, so point it at the new one.
Change the PHP-FPM socket in your Panel's configuration, then reload NGINX:
```bash
sed -i -e 's/php8\.[0-9]-fpm\.sock/php8.4-fpm.sock/' /etc/nginx/sites-available/pterodactyl.conf
systemctl reload nginx
```
Disable the old PHP module, enable the new one, and restart Apache:
```bash
apt -y install libapache2-mod-php8.4
a2dismod php8.3
a2enmod php8.4
systemctl restart apache2
```
Replace `php8.3` with the version you are upgrading from.
Change the PHP-FPM socket in `/etc/caddy/Caddyfile`, then reload Caddy:
```bash
sed -i -e 's/php8\.[0-9]-fpm\.sock/php8.4-fpm.sock/' /etc/caddy/Caddyfile
systemctl reload caddy
```
## Restarting the Queue Worker [#restarting-the-queue-worker]
The queue worker runs the old version until you restart it:
```bash
systemctl restart pteroq
```
## Checking the Panel [#checking-the-panel]
To confirm the new version has everything the Panel needs, run:
```bash
cd /var/www/pterodactyl
composer check-platform-reqs --no-dev
```
Then open the Panel in your browser. Once everything works, you can remove the old version, for example with `apt -y purge 'php8.3*'`.
# Building Panel Assets
## Introduction [#introduction]
The Panel's frontend is written in React and TypeScript. The release archive ships it prebuilt, with its source in `resources/scripts`; if you change the source, rebuild the frontend to see the change.
Updating the Panel replaces its source files, so your changes are lost. First check whether an [extension](../../extensions/index.mdx) or a [theme](./themes.mdx) can do what you need. Both keep working across updates.
Build on a development or staging machine if you can. Building uses a lot of memory and CPU for a few minutes.
## Installing Node.js [#installing-nodejs]
Building needs Node.js 22.12 or newer. Ubuntu's package is too old, so install it from NodeSource:
```bash
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt -y install nodejs
```
## Building the Frontend [#building-the-frontend]
Install the build tools, then build the frontend:
```bash
cd /var/www/pterodactyl
npm ci
npm run build:production
```
The build writes to `public/assets`. Give the web server user ownership of the files again, then reload the Panel in your browser:
```bash
chown -R www-data:www-data /var/www/pterodactyl/*
```
## Working on the Frontend [#working-on-the-frontend]
While working on a change, run this to rebuild the frontend every time you save a file:
```bash
npm run watch
```
Stop it with `CTRL+C`, then run `npm run build:production` when you are done.
# Themes
## Introduction [#introduction]
A theme changes the Panel's colors, corner roundness, and console colors. It is a small folder with a CSS file, so it needs no frontend build and keeps working when you update the Panel.
Themes only change appearance. To add pages or features, use an [extension](../../extensions/index.mdx).
## Creating a Theme [#creating-a-theme]
Themes live in the Panel's `themes` directory. Each theme is a folder with two files:
```text
/var/www/pterodactyl/themes/
└── midnight/
├── theme.json
└── tokens.css
```
`theme.json` names the theme. Its `id` must match the folder's name, start with a letter, and use only lowercase letters, numbers, and dashes:
```json title="theme.json"
{
"id": "midnight",
"name": "Midnight",
"version": "1.0.0"
}
```
`name` and `version` are optional.
`tokens.css` sets the colors you want to change. The Panel uses its dark colors by default, so put your values in a `.dark` block:
```css title="tokens.css"
.dark {
--background: oklch(0.2 0.03 265);
--card: oklch(0.26 0.03 265);
--primary: oklch(0.65 0.2 300);
--accent: oklch(0.75 0.15 200);
--radius: 0.25rem;
}
```
Set only the tokens you want to change. Colors can use any CSS color format, such as `oklch()` or `#hex`.
### Images and Fonts [#images-and-fonts]
Put images and fonts in an `assets` folder inside the theme. They are published to `/assets/theme/`. `tokens.css` is an ordinary stylesheet, so you can add your own rules:
```css
body {
background-image: url('/assets/theme/background.png');
background-size: cover;
}
```
Tokens hold only colors and sizes, not images.
## Applying a Theme [#applying-a-theme]
List the themes the Panel can find:
```bash
cd /var/www/pterodactyl
php artisan p:theme:list
```
Apply one by its ID:
```bash
php artisan p:theme:apply midnight
```
This copies `tokens.css` to `public/assets/theme.css` and the `assets` folder to `public/assets/theme`. The theme loads after the Panel's styles, so its values win. Reload the Panel in your browser to see it.
After you change a theme's files, run `p:theme:apply` again.
To restore the Panel's own look, run:
```bash
php artisan p:theme:reset
```
Only one theme applies at a time, and users cannot choose their own.
## Tokens [#tokens]
A theme may set these tokens.
### Colors [#colors]
| Token | Controls |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `--background` | The page background. |
| `--foreground` | Main text and icons. |
| `--muted-foreground` | Less important text. |
| `--card`, `--card-foreground` | Cards, panels, table rows, and their text. |
| `--popover`, `--popover-foreground` | Dialogs, dropdown menus, and their text. |
| `--muted` | Quiet backgrounds, such as code blocks. |
| `--primary`, `--primary-foreground` | Main buttons and their text. |
| `--secondary`, `--secondary-foreground` | Secondary buttons and their text. |
| `--accent`, `--accent-foreground` | Links, active items, and highlights. |
| `--border` | Borders and dividers. |
| `--input` | Borders and backgrounds of form fields. |
| `--ring` | The outline around focused elements. |
| `--destructive`, `--success`, `--warning` | Error, success, and warning messages and buttons. Each has a matching `-foreground` token for its text. |
| `--chart-1` to `--chart-5` | Charts and generated avatars. |
The `--sidebar` tokens, such as `--sidebar` and `--sidebar-accent`, follow the matching tokens above by default.
### Shape [#shape]
| Token | Controls |
| ---------- | ---------------------------------------------------------------------------------- |
| `--radius` | How round corners are. The default is `0.5rem`; other corner sizes derive from it. |
### Console [#console]
| Token | Controls |
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `--terminal-background` | The console's background. |
| `--terminal-selection`, `--terminal-selection-foreground` | Selected text in the console. |
| `--terminal-ansi-black` to `--terminal-ansi-white` | The eight standard console colors: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, and `white`. |
| `--terminal-ansi-bright-black` to `--terminal-ansi-bright-white` | The eight bright console colors. |
Console colors apply the next time the console opens.
# Building Wings
## Introduction [#introduction]
Wings is written in Go, so you can change its source and compile your own binary. This guide covers building only; changing the code requires knowing Go.
Build and test your binary on a development machine before using it on a node that runs servers for other people.
## Requirements [#requirements]
You need Git, and Go at the version shown on the `go` line of Wings' [`go.mod`](https://github.com/pterodactyl/wings/blob/develop/go.mod) file, or newer. Follow Go's [installation instructions](https://go.dev/doc/install).
Wings runs only on Linux, so building on Linux is the simplest way to get a working binary.
## Building [#building]
Clone the source and build Wings:
```bash
git clone https://github.com/pterodactyl/wings.git
cd wings
go build -o wings wings.go
```
This creates a `wings` binary in the current directory, built for the current machine. To build for a node with a different CPU, set `GOARCH`, for example `GOOS=linux GOARCH=arm64 go build -o wings wings.go`.
## Installing Your Binary [#installing-your-binary]
Back up the current binary, then install yours:
```bash
systemctl stop wings
mv /usr/local/bin/wings /usr/local/bin/wings-backup
cp ./wings /usr/local/bin/wings
chmod u+x /usr/local/bin/wings
systemctl start wings
```
If Wings does not start, stop the service and run it in the foreground to see its errors:
```bash
systemctl stop wings
wings --debug
```
To roll back, move `wings-backup` back to `/usr/local/bin/wings` and start the service again.
# Creating a Custom Egg
## Introduction [#introduction]
An egg tells the Panel how to install and run one kind of server, such as a Minecraft server or a Discord bot: its Docker image, startup command, user-editable settings, and install script. You organize eggs with [tags](../../project/how-it-works.mdx#terms).
Do not edit the eggs that ship with the Panel. Every Panel update replaces them, and your changes are lost. To change one, export it, import it as a new egg, and edit the copy.
Manage eggs under **Admin → Eggs**. There are three ways to add one:
* **Browse Catalog** imports eggs from the community catalog at [eggs.pterodactyl.io](https://eggs.pterodactyl.io).
* **Import Egg** uploads an egg file you already have.
* **New Egg** creates an egg from scratch.
## Importing an Egg [#importing-an-egg]
### From the Catalog [#from-the-catalog]
Click **Browse Catalog**. Search by name or description, or pick a **Category**, then click **Import** on the egg you want. The Panel adds it with its variables and install script.
The catalog is cached for an hour. Click **Refresh Catalog** to load the latest list.
### From a File [#from-a-file]
Click **Import Egg**, choose the egg's `.json` file, and click **Import Egg**. Eggs exported from Pterodactyl 1.x and 2.x are accepted.
An imported egg has no tags. Add them on the egg's **Tags** tab.
## Creating an Egg [#creating-an-egg]
Click **New Egg**. The form has two sections.
### Configuration [#configuration]
* **Name:** The name users see as their server's type.
* **Description:** A description of the egg.
* **Force Outgoing IP:** Sends the server's outgoing traffic from its primary allocation's IP address. Use it for games that check the address a server connects from. It also blocks the server from reaching other servers on the node over the internal network.
* **Docker Images:** The images servers may run in, one per line. See [Docker Images](./egg-docker-images.mdx).
* **Startup Command:** The command that starts the server. See [Startup and Configuration](./egg-config-parser.mdx).
* **Features:** Optional, comma-separated names of [features](#features) that help users fix common problems, such as `eula` for Minecraft.
### Process Management [#process-management]
These settings control how Wings starts, watches, and stops the server. See [Startup and Configuration](./egg-config-parser.mdx) for each one.
Click **Create Egg**. The Panel opens the new egg, where you add its [variables](./egg-variables.mdx) and [install script](./egg-install-script.mdx).
### Features [#features]
A feature watches the console for a known problem and shows the user how to fix it. Built-in features:
| Feature | What It Does |
| ------------------ | --------------------------------------------------------------------------------------- |
| `eula` | Asks the user to accept Minecraft's EULA when the server will not start without it. |
| `java_version` | Lets the user pick another Docker image when the server needs a different Java version. |
| `gsl_token` | Warns when the server's Steam Game Server Login Token is missing or invalid. |
| `pid_limit` | Explains that the server has started too many processes. |
| `steam_disk_space` | Warns when SteamCMD runs out of disk space. |
| `hytale_oauth` | Shows the sign-in link when a Hytale server asks for authentication. |
Extensions may add their own features.
## Managing an Egg [#managing-an-egg]
An egg's page has four tabs:
* **Configuration:** The settings you entered at creation.
* **Tags:** The egg's tags.
* **Variables:** Per-server settings that users and administrators can change. See [Egg Variables](./egg-variables.mdx).
* **Install Script:** The script that installs the server. See [Install Scripts](./egg-install-script.mdx).
Buttons at the top of the page:
* **Export** downloads the egg as a `.json` file. Settings inherited from another egg are written in, so the file is self-contained.
* **Update From File** replaces the egg's settings with a file's. Variables missing from the file are removed. Servers already using the egg keep their startup command and Docker image.
* **Delete Egg** deletes the egg. You cannot delete an egg that servers use, or that other eggs copy settings from.
## Organizing Eggs With Tags [#organizing-eggs-with-tags]
Tags group related eggs, such as every Minecraft egg. To create one, open **Admin → Tags**, click **New Tag**, and enter:
* **Name:** The label shown throughout the Panel.
* **Slug:** A short, stable API identifier, such as `minecraft`. It cannot be only numbers.
* **Color:** Optional. A hexadecimal color.
To assign tags, open an egg's **Tags** tab, choose tags under **Assigned Tags**, and click **Save tags**.
These slugs are reserved for built-in game tags: `srcds`, `bedrock`, `minecraft`, `rust`, `garrysmod`, `fivem`, and `voice`. A tag created with one of them gets its name and color from the Panel, such as **Minecraft** for `minecraft`, and cannot be edited or deleted.
A new installation has no tags, and the bundled eggs have none assigned. Upgrading from 1.x creates tags from your nests. See [Nests Are Replaced by Tags](../../upgrading/changes-from-v1.mdx#nests-are-replaced-by-tags).
# Creating a Custom Image
## Introduction [#introduction]
If none of the [official images](./egg-docker-images.mdx) has what your server needs, build your own. This guide builds a small Debian-based image the same way the official images are built.
You need Docker on the build machine, either a node or your own computer.
## How Wings Runs an Image [#how-wings-runs-an-image]
The image must work with how Wings runs containers:
* **The server's files are at `/home/container`.** Wings mounts the server's directory there. Set the image's working directory to `/home/container`.
* **The server does not run as root.** Wings runs the container as its `pterodactyl` system user, whose user ID differs between nodes. Do not rely on root, or on a specific user name or ID.
* **The startup command is in the `STARTUP` variable.** Wings does not run the command itself. It passes it in the `STARTUP` environment variable, with the egg's variables still written as `{{NAME}}`. The entrypoint must expand and run it.
* **The stop command `^C` goes to the main process.** If the entrypoint starts the server with `exec`, the server receives the signal and can shut down cleanly.
## The Dockerfile [#the-dockerfile]
Create a directory for the image containing a `Dockerfile`:
```dockerfile title="Dockerfile"
FROM --platform=$TARGETOS/$TARGETARCH debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl iproute2 tzdata \
&& rm -rf /var/lib/apt/lists/* \
&& useradd -m -d /home/container container
USER container
ENV USER=container HOME=/home/container
WORKDIR /home/container
COPY --chmod=755 ./entrypoint.sh /entrypoint.sh
CMD ["/bin/bash", "/entrypoint.sh"]
```
Install everything the server needs to run in the `RUN` step. The `container` user and the `USER` and `HOME` variables give programs a normal home directory.
## The Entrypoint [#the-entrypoint]
Next to the `Dockerfile`, create `entrypoint.sh`:
```bash title="entrypoint.sh"
#!/bin/bash
cd /home/container || exit 1
# Use UTC unless Wings set a time zone.
export TZ=${TZ:-UTC}
# The container's own IP address, for servers that need to know it.
INTERNAL_IP=$(ip route get 1 | awk '{for (i = 1; i < NF; i++) if ($i == "src") { print $(i + 1); exit }}')
export INTERNAL_IP
# Turn {{NAME}} into ${NAME}, then fill in the values.
PARSED=$(echo "${STARTUP}" | sed -e 's/{{/${/g' -e 's/}}/}/g' | eval echo "$(cat -)")
# Show the command in the console, then replace this script with it.
printf "\033[1m\033[33mcontainer@pterodactyl~ \033[0m%s\n" "$PARSED"
exec env ${PARSED}
```
Like the official images, the last line runs the command directly, not through a shell, so `&&` and `|` do not work in the startup command. If an egg needs them, change the last line to `exec bash -c "${PARSED}"`.
## Building the Image [#building-the-image]
Build and tag the image. To use it on other nodes, push it to a registry such as the GitHub Container Registry:
```bash
docker build -t ghcr.io/your-name/my-image:latest .
docker push ghcr.io/your-name/my-image:latest
```
Then add `ghcr.io/your-name/my-image:latest` to the egg's **Docker Images**.
To test on one node without a registry, build the image on that node and prefix its name with `~`, such as `~my-image:latest`. Wings then uses the local image instead of pulling it. See [Local Images](./egg-docker-images.mdx#local-images).
# Startup and Configuration
## Introduction [#introduction]
These settings tell Wings how to start a server, detect that it has started, stop it, and keep its configuration files up to date. They are in the **Configuration** and **Process Management** cards on the egg's **Configuration** tab.
## Startup Command [#startup-command]
The **Startup Command** runs every time the server starts. Use `{{VARIABLE}}` to insert one of the egg's [variables](./egg-variables.mdx):
```text
java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}
```
The Panel shows users the command with the values filled in.
This is the default for new servers. Each server keeps its own copy, so changing it here does not affect existing servers. To change a server's command, use its **Startup** tab in the admin area.
The official images run the startup command directly, not through a shell, so `&&`, `|`, and `>` do not work. If you need them, start the command with `bash -c`, or put the commands in a script the install script creates.
## Stop Command [#stop-command]
Wings sends the **Stop Command** to stop the server gracefully. It is either:
* a command typed into the server's console, such as `stop` for Minecraft, or
* `^C`, which sends the process an interrupt signal (`SIGINT`), like pressing Ctrl+C in a terminal.
If the server does not stop in time, users can kill it from the Panel.
## Start Configuration [#start-configuration]
The **Start Configuration** tells Wings when the server has started. The Panel shows **Starting** until one of the `done` strings appears in the console, then **Running**:
```json
{
"done": "Server started on port"
}
```
`done` can be a list if the server prints different messages in different situations:
```json
{
"done": [
"Done (",
"Server is ready"
]
}
```
If no string ever appears, the server stays **Starting**, so match the server's real output.
## Log Configuration [#log-configuration]
Leave **Log Configuration** as `{}`. Wings shows the server's console output without it.
## Configuration Files [#configuration-files]
**Configuration Files** lets Wings set values in the server's configuration files every time it starts. For example, it can keep a game server on its assigned port, whatever a user writes in the file.
The value is a JSON object. Each key is a file path relative to the server's directory, with the parser to use and the settings to set:
```json
{
"server.properties": {
"parser": "properties",
"find": {
"server-ip": "0.0.0.0",
"server-port": "{{server.build.default.port}}",
"max-players": "{{env.MAX_PLAYERS}}"
}
}
}
```
On start, Wings sets each key to its value. Most parsers add missing keys and create the file if it does not exist. The `file` parser skips a missing file, and the `json` parser fails on the empty file Wings creates, so have the install script create those files.
### Values [#values]
A value can be plain text or include these placeholders:
| Placeholder | Value |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{{server.build.default.ip}}` | The IP address of the server's primary allocation. |
| `{{server.build.default.port}}` | The port of the server's primary allocation. |
| `{{server.build.memory}}` | The server's memory limit, in MiB. |
| `{{env.VARIABLE}}` | An egg variable's value, such as `{{env.MAX_PLAYERS}}`. For port, IP address, and memory, use the `server` placeholders above; `{{env.SERVER_PORT}}` and similar end up in the file as placeholder text. |
| `{{config.docker.interface}}` | The node's address on the servers' Docker network, usually `172.18.0.1`. |
### Parsers [#parsers]
| Parser | For |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `properties` | `.properties` files, with `key=value` lines. |
| `ini` | INI files. Keys are `section.key`, such as `Server.Port`. Bracket section names that contain dots, such as `[/Script/Engine.GameSession].MaxPlayers`. |
| `yaml` | YAML files. Use dots for nested keys, such as `settings.port`, and brackets for list items, such as `listeners[0].host`. |
| `json` | JSON files. Keys work as for `yaml`. |
| `xml` | XML files. Keys are element paths, such as `Config.Port` for ``. Wings sets the element's text, or an attribute if the value is written as `[name='value']`. |
| `file` | Any text file. Replaces every line starting with the key with the value, so the value must be the whole line, such as `"port=": "port={{server.build.default.port}}"`. It does not add missing lines. Use it only when no other parser fits. |
In `yaml` and `json` keys, `*` matches every item at one level. For example, `servers.*.port` sets the port of every entry under `servers`. Only the first `*` in a key works.
## Copy Settings From [#copy-settings-from]
**Copy Settings From** makes an egg inherit another egg's Process Management settings. These fields, if left empty, come from the chosen egg: the stop command, the start, log, and configuration file settings, and the features.
This avoids repeating settings across similar eggs, such as several Minecraft server types. Copying goes one level only: settings the chosen egg copies from another egg are not passed on.
Only these settings are copied. The copying egg still needs its own startup command, Docker images, and variables.
# Docker Images
## Introduction [#introduction]
Every server runs in a Docker container, and the egg's image decides what it has installed, such as Java for a Minecraft server or Python for a bot. Choose an image with everything the server needs to **run**; tools needed only for installing belong in the [install script's container](./egg-install-script.mdx).
## Official Images [#official-images]
Pterodactyl publishes images, called yolks, at `ghcr.io/pterodactyl/yolks`. The source is in the [pterodactyl/yolks](https://github.com/pterodactyl/yolks) repository.
| Tag | Contents |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `debian`, `alpine` | A general-purpose Debian or Alpine system. |
| `java_8` to `java_25` | Java, for Minecraft and other Java programs. Tags ending in `j9`, such as `java_17j9`, use the OpenJ9 runtime. |
| `nodejs_12` to `nodejs_20` | Node.js. |
| `python_3.7` to `python_3.11` | Python. |
| `go_1.14` to `go_1.26`, `go_latest` | Go. |
For example, this image runs Java 21:
```text
ghcr.io/pterodactyl/yolks:java_21
```
The community maintains more images at `ghcr.io/ptero-eggs/yolks` ([Ptero-Eggs/yolks](https://github.com/Ptero-Eggs/yolks)), including newer language versions, .NET, Wine, and databases. The eggs in the [catalog](./creating-custom-egg.mdx#from-the-catalog) often use them.
To build your own image, see [Creating a Custom Image](./creating-custom-image.mdx).
## Offering Several Images [#offering-several-images]
To offer several images, enter one per line in the egg's **Docker Images** field. To name an image, put the name and a `|` before it:
```text
Java 17|ghcr.io/pterodactyl/yolks:java_17
Java 21|ghcr.io/pterodactyl/yolks:java_21
```
A line without a name shows the image itself.
When you create a server, you pick one of the egg's images or enter another. Users can switch between the egg's images on their server's **Startup** page; the change applies on the next start. If the server's image is not one of the egg's, users cannot change it.
## Local Images [#local-images]
Wings downloads an egg's image the first time a server needs it. For an image you built on the node that is not in any registry, put a `~` before its name, such as `~my-game-server:latest`. Wings then uses the local image and does not try to download it.
# Install Scripts
## Introduction [#introduction]
The install script runs when a server is created or reinstalled, and puts everything the server needs into its directory: the game server, its configuration files, and any mods. It runs in a temporary container separate from the server's own, and you set it up on the egg's **Install Script** tab.
## Script Settings [#script-settings]
* **Install Script:** The script itself.
* **Script Container:** The Docker image the script runs in. Pick one with the tools your script needs, such as `curl` or `unzip`.
* **Script Entrypoint Command:** The program that runs the script, usually `bash`. Alpine images do not include `bash`, so use `ash` with them.
* **Copy Script From:** Use another egg's install script. Leave **Install Script** empty when you do. See [Sharing a Script](#sharing-a-script).
* **Privileged Installation Script:** Kept for older daemon versions. Wings does not use it.
### Script Containers [#script-containers]
Pterodactyl publishes installer images with common tools:
* `ghcr.io/pterodactyl/installers:debian` includes `bash`, `curl`, `wget`, `git`, and `tar`.
* `ghcr.io/pterodactyl/installers:alpine` includes `curl`, `wget`, `git`, `tar`, `unzip`, and `jq`, but not `bash`.
The community publishes more at `ghcr.io/ptero-eggs/installers`, with `debian`, `alpine`, and `ubuntu` tags. Any other image works too, such as `eclipse-temurin:21-jdk` if the install needs Java.
## Writing the Script [#writing-the-script]
The server's files are at `/mnt/server` in the install container. **Only what the script puts in `/mnt/server` is kept.** Anything else, such as installed packages, is lost when the script finishes. Packages the server needs to run belong in its [Docker image](./egg-docker-images.mdx).
The egg's [variables](./egg-variables.mdx) are available as environment variables, along with `SERVER_MEMORY`, `SERVER_IP`, and `SERVER_PORT`.
A typical script moves into `/mnt/server`, downloads the server, and writes a first configuration:
```bash
#!/bin/bash
mkdir -p /mnt/server
cd /mnt/server || exit 1
curl -sSL "https://example.com/downloads/server-${SERVER_VERSION}.tar.gz" | tar -xz
echo "max_players=${MAX_PLAYERS}" > server.cfg
```
The script runs as root. When it finishes, Wings gives the files to the user its servers run as.
When a user or administrator chooses **Reinstall**, the script runs again over the server's existing files. Do not overwrite files users have changed, such as worlds, unless you intend to.
## Checking the Output [#checking-the-output]
The server's console shows the script's output while it runs. Wings also saves the latest install's output on the node:
```bash
cat /var/log/pterodactyl/install/.log
```
Wings does not check whether the script succeeded. The server is marked installed even if a command fails or the script exits with an error. Check a new script's output and fix errors before reinstalling.
## Sharing a Script [#sharing-a-script]
When several eggs install the same way, such as different Minecraft server types, write the script in one egg and select that egg under **Copy Script From** in the others. Leave their **Install Script** empty: if an egg has its own script, that script runs instead.
An egg that copies its script cannot be copied from. The **Eggs Using This Script** card lists the eggs copying an egg's script.
# Egg Variables
## Introduction [#introduction]
Variables are per-server settings, such as the game version or the maximum number of players. Each becomes an environment variable in the server's container, usable in the startup command, install script, and configuration files.
Manage them on the egg's **Variables** tab. The Panel also sets some variables for every server; see [Built-In Variables](#built-in-variables).
## Creating a Variable [#creating-a-variable]
Click **Create Variable** and fill in:
* **Name:** A friendly name users see, such as `Max Players`.
* **Description:** Tells users what the variable does.
* **Environment Variable:** The name inside the container, such as `MAX_PLAYERS`. Letters, numbers, and underscores only. Wings passes the name in uppercase, so use uppercase names to avoid confusion.
* **Default Value:** The value new servers start with.
* **Users Can View:** Whether users see the variable on their server's **Startup** page.
* **Users Can Edit:** Whether users may change the value.
* **Input Rules:** [Laravel validation rules](https://laravel.com/docs/validation#available-validation-rules) that every value must pass.
Click **Create Variable** to save it.
Hiding a variable from users does not make it secret. Its value is still in the server's environment, where the server's processes can read it. Do not use variables for secrets users must not see.
## Using a Variable [#using-a-variable]
Refer to a variable by its environment variable name. The syntax depends on where you use it:
| Where | How | Example |
| ------------------- | -------------------- | -------------------------------------- |
| Startup command | `{{NAME}}` | `--max-players {{MAX_PLAYERS}}` |
| Install script | `$NAME` or `${NAME}` | `echo "players=${MAX_PLAYERS}"` |
| Configuration files | `{{env.NAME}}` | `"max-players": "{{env.MAX_PLAYERS}}"` |
Users see the startup command with values filled in for `{{SERVER_MEMORY}}`, `{{SERVER_IP}}`, `{{SERVER_PORT}}`, and the egg's own variables, and `[hidden]` for variables they cannot view. Placeholders are case-sensitive; any other placeholder stays as written.
## Input Rules [#input-rules]
Separate rules with `|`. The most common:
| Rule | Meaning |
| ------------------------ | ---------------------------------------------------------------- |
| `required` | A value must be given. |
| `nullable` | The value may be empty. |
| `string` | The value is text. |
| `numeric` | The value is a number. |
| `integer` | The value is a whole number. |
| `boolean` | The value is `1` or `0`. Values arrive as text, so `true` fails. |
| `between:1,64` | A number from 1 to 64, or text of 1 to 64 characters. |
| `max:20` | A number up to 20, or text of up to 20 characters. |
| `in:survival,creative` | One of the listed values. |
| `regex:/^[\w.-]+\.jar$/` | Matches a regular expression. |
For example, `required|string|max:20` requires text of at most 20 characters. New variables start with these rules.
Details that decide whether a value passes:
* `between`, `max`, and `min` compare the number only when the rules also include `integer` or `numeric`. Otherwise they count characters, so `required|max:128` accepts `500`.
* Values are trimmed at both ends, and an empty value reaches the rules as null. Without `nullable`, rules such as `string` fail on it.
* Rules are split at every `|`, so a `|` inside a regular expression cuts it in two. Leave `|` out of patterns.
The rules also set how the variable appears on the **Startup** page:
* `boolean`, `in:0,1`, or `in:true,false` shows a switch. If its rules also include `string`, the switch saves `true` or `false`; otherwise it saves `1` or `0`. The `boolean` rule rejects `true` and `false`, so pair `string` with `in:true,false` instead.
* Any other `in:` rule shows a list of the allowed values.
* Everything else shows a text field.
## Reserved Names [#reserved-names]
These environment variable names are reserved in any letter case:
`SERVER_MEMORY`, `SERVER_IP`, `SERVER_PORT`, `ENV`, `HOME`, `USER`, `STARTUP`, `SERVER_UUID`, and `UUID`.
## Built-In Variables [#built-in-variables]
Every server's container gets these variables, whatever its egg:
| Variable | Value | Example |
| --------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------- |
| `STARTUP` | The startup command, with variables not yet filled in. | `java -jar {{SERVER_JARFILE}}` |
| `SERVER_MEMORY` | The server's memory limit, in MiB. | `1024` |
| `SERVER_IP` | The primary allocation's IP address. Wings replaces `127.0.0.1` with the Docker network interface. | `192.168.1.10` |
| `SERVER_PORT` | The primary allocation's port. | `25565` |
| `TZ` | The node's time zone. Wings detects it, or you can set `system.timezone` in Wings' configuration. | `Europe/London` |
| `P_SERVER_UUID` | The server's UUID. | `6d1f7929-5c2e-4cd4-99af-924dacb15537` |
| `P_SERVER_LOCATION` | The node location's short code. | `us1` |
| `P_SERVER_ALLOCATION_LIMIT` | How many allocations the server may have. Written with decimals, and empty when there is no limit. | `1.000000` |
Wings sets `TZ`, `STARTUP`, `SERVER_MEMORY`, `SERVER_IP`, and `SERVER_PORT` first and skips an egg variable with any of those names.
The official Docker images also set `USER=container` and `HOME=/home/container`.
## Changing a Server's Variables [#changing-a-servers-variables]
Users change the variables they may edit on their server's **Startup** page. Administrators can change all of a server's variables on its **Startup** tab in the admin area. New values apply on the next start.
# Minecraft Proxy Networks
## Introduction [#introduction]
A Minecraft proxy, such as BungeeCord, Waterfall, or Velocity, lets players move between several backend servers through one address. Players connect to the proxy, and the proxy connects to the backends.
Backend servers usually run in offline mode and trust the proxy to check who each player is, so anyone who connects to them directly can join as any player. This guide hides them from the internet so they only accept connections from the proxy.
The proxy and all of its backend servers must run on the same node.
Every server on the node can connect to the backend servers. If you host servers for other people, run only one proxy network per node so one customer's servers cannot reach another's.
## Allocations [#allocations]
Give the proxy a normal allocation on the node's public IP address so players can reach it.
Give each backend server an allocation on `127.0.0.1` instead. To create them, open the node's **Allocation** tab, enter `127.0.0.1` as the **IP Address**, and add a port for each backend, such as `25566-25570`.
For these allocations, Wings listens on the node's address on the servers' Docker network, `172.18.0.1` by default, not on `127.0.0.1`. Other servers on the node can reach that address; nothing outside the node can.
## Configuring the Proxy [#configuring-the-proxy]
Inside a container, `127.0.0.1` and `localhost` mean the container itself, not the node. In the proxy's configuration, address each backend as `172.18.0.1` with its port.
For BungeeCord and Waterfall, in `config.yml`:
```yaml title="config.yml"
servers:
lobby:
address: 172.18.0.1:25566
restricted: false
motd: Lobby
survival:
address: 172.18.0.1:25567
restricted: false
motd: Survival
```
For Velocity, in `velocity.toml`:
```toml title="velocity.toml"
[servers]
lobby = "172.18.0.1:25566"
survival = "172.18.0.1:25567"
try = ["lobby"]
```
## Configuring the Backend Servers [#configuring-the-backend-servers]
Configure each backend as your proxy requires. For BungeeCord and Waterfall with Paper or Spigot:
* In `server.properties`, set `online-mode` to `false`.
* In `spigot.yml`, set `settings.bungeecord` to `true`.
Velocity's recommended forwarding mode needs different settings. See your proxy's documentation.
## Firewalls [#firewalls]
If UFW is enabled, it blocks connections from the servers' network to `172.18.0.1`, so the proxy cannot reach the backends. Allow each backend's port from the `pterodactyl0` interface:
```bash
ufw allow in on pterodactyl0 to 172.18.0.1 port 25566 proto tcp
```
Replace `25566` with the backend's port and repeat for each backend. The rule only applies to traffic from the servers' network, so the backends stay hidden from the internet.
# Artisan Commands
## Introduction [#introduction]
Artisan is the command-line interface of Laravel, the framework the Panel is built on. The Panel's own commands start with `p:`. Run them from the Panel's directory:
```bash
cd /var/www/pterodactyl
php artisan list p
```
To see a command's options, use `help`:
```bash
php artisan help p:user:make
```
Most commands prompt for any value you do not pass as an option. To run one without prompts, such as in a script, pass every option and add `--no-interaction`.
## Users [#users]
### Creating a User [#creating-a-user]
```bash
php artisan p:user:make
```
The command asks for the email address, username, first and last name, password, and whether the user is an administrator. You can pass these as options instead:
```bash
php artisan p:user:make --email=jane@example.com --username=jane --name-first=Jane --name-last=Doe --admin=0
```
The command still asks for the password, to keep it out of your shell history. To create the user without a password, add `--no-password`. The Panel then emails the user a link to set one.
### Deleting a User [#deleting-a-user]
```bash
php artisan p:user:delete --user=jane@example.com
```
`--user` matches the start of a user's ID, username, or email address. The command lists the matches and asks which one to delete. A user who still owns servers cannot be deleted.
### Disabling Two-Factor Authentication [#disabling-two-factor-authentication]
If a user loses their two-factor device and recovery codes, turn off two-factor authentication for them:
```bash
php artisan p:user:disable2fa --email=jane@example.com
```
Confirm the user's identity first. Afterward, anyone who knows the user's password can sign in.
## Locations and Nodes [#locations-and-nodes]
### Creating and Deleting Locations [#creating-and-deleting-locations]
```bash
php artisan p:location:make --short=us1 --long="United States, East"
php artisan p:location:delete --short=us1
```
A location that still has nodes cannot be deleted.
### Creating a Node [#creating-a-node]
```bash
php artisan p:node:make
```
The command asks for the same details as the admin area's **New node** page, including the location ID, domain name, ports, and resource limits. Run `php artisan help p:node:make` for the matching options.
### Listing Nodes [#listing-nodes]
```bash
php artisan p:node:list
```
Add `--format=json` for JSON output.
### Showing a Node's Configuration [#showing-a-nodes-configuration]
Prints a node's Wings configuration, the same file its **Configuration** tab shows. Pass the node's ID or UUID:
```bash
php artisan p:node:configuration 1
```
If the Panel and Wings run on the same machine, you can save it straight to Wings' configuration file:
```bash
php artisan p:node:configuration 1 > /etc/pterodactyl/config.yml
```
Add `--format=json` to print it as JSON.
## Servers [#servers]
### Power Actions for Many Servers [#power-actions-for-many-servers]
`p:server:bulk-power` sends `start`, `stop`, `restart`, or `kill` to many servers at once:
```bash
# Restart every server.
php artisan p:server:bulk-power restart
# Restart servers 1, 2, and 3.
php artisan p:server:bulk-power restart --servers=1,2,3
# Stop every server on nodes 1 and 2.
php artisan p:server:bulk-power stop --nodes=1,2
```
`--servers` and `--nodes` take IDs. Without either, the action applies to every server. The command first shows how many servers it will affect and asks you to confirm.
## The Panel [#the-panel]
### Showing the Panel's Configuration [#showing-the-panels-configuration]
```bash
php artisan p:info
```
Shows the Panel's version, address, and database, cache, and email settings. Include this output when you ask for help, with any passwords removed.
### Changing the Environment [#changing-the-environment]
These commands update your `.env` file. They are the same ones you ran during installation:
```bash
php artisan p:environment:setup
php artisan p:environment:database
php artisan p:environment:mail
```
### Updating the Panel [#updating-the-panel]
```bash
php artisan p:upgrade --release=
```
Downloads a new release and runs the update steps for you. Until 2.0 is released, use the manual steps instead. See [Updating the Panel](../../panel/updating.mdx#updating-with-one-command).
### Viewing Telemetry Data [#viewing-telemetry-data]
```bash
php artisan p:telemetry
```
Prints the anonymous usage data the Panel sends when telemetry is enabled, without sending it. See [Telemetry](../../panel/additional-configuration.mdx#telemetry).
## Extensions and Themes [#extensions-and-themes]
The `p:extension` commands install, enable, disable, and remove extensions. See [Extensions](../../extensions/index.mdx).
The `p:theme` commands apply a theme to the Panel. See [Themes](../customization/themes.mdx).
# Setting Up MySQL
## Introduction [#introduction]
The Panel uses MySQL or MariaDB in two ways:
* **The Panel's own database** stores users, servers, and settings. You create it once, when you install the Panel.
* **Database hosts** are optional. They let the Panel create databases for game servers, such as for a Minecraft plugin.
This guide uses MariaDB, which the installation guides install. The SQL is the same for MySQL.
## Signing In to MariaDB [#signing-in-to-mariadb]
Run the MariaDB client as root:
```bash
mariadb -u root
```
On a new installation, the system's root user signs in without a password. If you set a root password, add `-p` and the client asks for it.
## Creating the Panel's Database [#creating-the-panels-database]
Create a database and a user for the Panel. Replace `yourPassword` with a strong, unique password:
```sql
CREATE USER 'pterodactyl'@'127.0.0.1' IDENTIFIED BY 'yourPassword';
CREATE DATABASE panel;
GRANT ALL PRIVILEGES ON panel.* TO 'pterodactyl'@'127.0.0.1' WITH GRANT OPTION;
```
The user can only sign in from `127.0.0.1`, the same machine. If the database runs on a different machine from the Panel, use the Panel's IP address instead.
If you followed [Getting Started](../../panel/getting-started.mdx#creating-the-database), you already ran these statements.
## Creating a Database Host [#creating-a-database-host]
A database host is a MariaDB server where the Panel creates databases for game servers. It can be the Panel's own MariaDB server or a separate one.
### Creating a User for the Panel [#creating-a-user-for-the-panel]
The Panel needs a MariaDB user that can create databases and users. Do not reuse the Panel's own database user.
MariaDB only lets a user sign in from the address it was created for, and the Panel connects from the Panel server's IP address. On the database host, sign in to MariaDB and run the statements below. Replace `203.0.113.10` with the Panel server's IP address and `yourPassword` with a strong, unique password:
```sql
CREATE USER 'pterodactyluser'@'203.0.113.10' IDENTIFIED BY 'yourPassword';
GRANT ALL PRIVILEGES ON *.* TO 'pterodactyluser'@'203.0.113.10' WITH GRANT OPTION;
```
Use the IP address even when the database runs on the Panel's machine. The Panel connects to the address you enter as **Host** below, and when that is the machine's own IP address, the connection comes from that address, not from `127.0.0.1`.
### Allowing Connections From Game Servers [#allowing-connections-from-game-servers]
Game servers connect from inside their containers, so MariaDB must accept connections from addresses other than `127.0.0.1`.
By default, MariaDB only listens on `127.0.0.1`. To listen on every address, create `/etc/mysql/mariadb.conf.d/99-pterodactyl.cnf`:
```ini title="/etc/mysql/mariadb.conf.d/99-pterodactyl.cnf"
[mysqld]
bind-address=0.0.0.0
```
Then restart MariaDB:
```bash
systemctl restart mariadb
```
If you use a firewall, allow port `3306` only from addresses that need it, such as your nodes. Game servers on the database's machine connect through `172.18.0.1`.
### Adding the Host to the Panel [#adding-the-host-to-the-panel]
Open **Admin → Database Hosts**, click **New Database Host**, and fill in:
* **Name:** A name to identify the host.
* **Host:** The MariaDB server's IP address, such as its public IP address. The Panel connects to this address and shows it to users as the address to connect to, so it must work for both.
* **Port:** Usually `3306`.
* **Linked Node:** Optional. Servers on this node use this host by default.
* **Username** and **Password:** The user you created above.
Click **Create Host**. The Panel tests the connection before it saves the host.
## Creating Databases for Servers [#creating-databases-for-servers]
A server's owner creates databases on the server's **Databases** page, up to the server's **Database Limit**. The limit is `0` by default. Set it when you create the server, or later on the server's **Build** tab in the admin area.
The Panel creates the database on the host linked to the server's node, or on any host if none is linked. To stop the Panel from choosing a host that is not linked to the node, set this in your `.env` file:
```bash
PTERODACTYL_CLIENT_DATABASES_ALLOW_RANDOM=false
```
Administrators can also create databases for a server, and choose the host, on the server's **Database** tab in the admin area.
# Creating SSL Certificates
## Introduction [#introduction]
If the Panel uses HTTPS, Wings must too, because browsers do not let a secure page connect to an insecure service. You usually need one certificate for the Panel's domain and one for each node's domain.
This guide uses [Certbot](https://certbot.eff.org/) to get free certificates from Let's Encrypt. Certbot stores each one in `/etc/letsencrypt/live//`, where the Panel's [web server configurations](../../panel/webserver-configuration.mdx) and Wings expect it.
Caddy gets and renews the Panel's certificate itself. If you use Caddy, you only need this guide for your nodes.
## Installing Certbot [#installing-certbot]
Install Certbot and the plugin for your web server:
```bash
apt update
# For NGINX:
apt -y install certbot python3-certbot-nginx
# For Apache:
apt -y install certbot python3-certbot-apache
```
On a node without a web server, install `certbot` on its own.
## Creating a Certificate [#creating-a-certificate]
Let's Encrypt verifies that you control the domain by connecting to it on port `80`. The domain must point at the server, and port `80` must be open.
For the Panel, use your web server's plugin. `--deploy-hook` reloads the web server after each renewal so it uses the new certificate. Replace `panel.example.com` with your Panel's domain:
```bash
# NGINX:
certbot certonly --nginx -d panel.example.com --deploy-hook "systemctl reload nginx"
# Apache:
certbot certonly --apache -d panel.example.com --deploy-hook "systemctl reload apache2"
```
For a node without a web server, Certbot runs a temporary web server on port `80`, now and at each renewal. Wings reads its certificate at startup, so the hook restarts it:
```bash
certbot certonly --standalone -d node.example.com --deploy-hook "systemctl restart wings"
```
If the Panel and Wings share a server and a domain, one certificate serves both. Create it with your web server's plugin and both hooks: `--deploy-hook "systemctl reload nginx && systemctl restart wings"`.
## Using Cloudflare [#using-cloudflare]
If your domain uses Cloudflare's proxy, or port `80` is not reachable, Let's Encrypt cannot connect to your server. Certbot can instead prove that you control the domain by adding a DNS record through Cloudflare's API.
Create an API token: in the Cloudflare dashboard, open **My Profile → API Tokens**, click **Create Token**, and use the **Edit zone DNS** template. Under **Zone Resources**, select your domain, then create and copy the token.
Install the Cloudflare plugin and save the token in a file only root can read:
```bash
apt -y install python3-certbot-dns-cloudflare
mkdir -p /root/.secrets
echo "dns_cloudflare_api_token = " > /root/.secrets/cloudflare.ini
chmod 600 /root/.secrets/cloudflare.ini
```
Create the certificate:
```bash
certbot certonly --dns-cloudflare --dns-cloudflare-credentials /root/.secrets/cloudflare.ini -d panel.example.com --deploy-hook "systemctl reload nginx"
```
## Renewing Certificates [#renewing-certificates]
Let's Encrypt certificates last 90 days. The Certbot package includes a timer that renews them before they expire and runs each certificate's deploy hook. To test renewal, run:
```bash
certbot renew --dry-run
```
If a certificate has expired, browsers show an insecure connection error for the Panel, or the Panel cannot connect to Wings. Run `certbot renew`, then reload your web server and restart Wings.
# List languages
Returns the languages available for panel locale selectors.
## GET /api/admin/languages
```json
{
"summary": "List languages",
"operationId": "adminListLanguages",
"description": "Returns the languages available for panel locale selectors.",
"parameters": [],
"responses": {
"200": {
"description": "Available languages returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminLanguagesResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# List extensions
Returns discovered extensions, invalid manifests, and their install state.
## GET /api/admin/extensions
```json
{
"summary": "List extensions",
"operationId": "adminListExtensions",
"description": "Returns discovered extensions, invalid manifests, and their install state.",
"parameters": [],
"responses": {
"200": {
"description": "Extensions returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminListExtensionsResponseBody"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Install extension
Installs an uploaded .pteroext or .zip extension package.
## POST /api/admin/extensions
```json
{
"summary": "Install extension",
"operationId": "adminInstallExtension",
"description": "Installs an uploaded .pteroext or .zip extension package.",
"parameters": [],
"responses": {
"201": {
"description": "Extension installed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminInstallExtensionResponseBody"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/AdminInstallExtensionRequest"
}
}
}
}
}
```
# Get extension settings
Returns the auto-render form schema (with current public values) for an extension that registered a settings definition.
## GET /api/admin/extensions/{extension}/settings
```json
{
"summary": "Get extension settings",
"operationId": "adminGetExtensionSettings",
"description": "Returns the auto-render form schema (with current public values) for an extension that registered a settings definition.",
"parameters": [],
"responses": {
"200": {
"description": "Settings schema returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminGetExtensionSettingsResponseBody"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Update extension settings
Validates against the extension's declared rules and persists the submitted values. Omitted fields keep their stored values.
## PATCH /api/admin/extensions/{extension_id}/settings
```json
{
"summary": "Update extension settings",
"operationId": "adminUpdateExtensionSettings",
"description": "Validates against the extension's declared rules and persists the submitted values. Omitted fields keep their stored values.",
"parameters": [],
"responses": {
"200": {
"description": "Settings updated; fresh schema returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateExtensionSettingsResponseBody"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateExtensionSettingsRequest"
}
}
}
}
}
```
# Enable extension
Enables an installed extension, publishes assets, and runs migrations.
## POST /api/admin/extensions/{extension}/enable
```json
{
"summary": "Enable extension",
"operationId": "adminEnableExtension",
"description": "Enables an installed extension, publishes assets, and runs migrations.",
"parameters": [],
"responses": {
"200": {
"description": "Extension enabled.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminEnableExtensionResponseBody"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Disable extension
Disables an installed extension without removing files or data.
## POST /api/admin/extensions/{extension}/disable
```json
{
"summary": "Disable extension",
"operationId": "adminDisableExtension",
"description": "Disables an installed extension without removing files or data.",
"parameters": [],
"responses": {
"200": {
"description": "Extension disabled.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminDisableExtensionResponseBody"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Remove extension
Deletes extension files, published assets, and the install record.
## DELETE /api/admin/extensions/{extension}
```json
{
"summary": "Remove extension",
"operationId": "adminRemoveExtension",
"description": "Deletes extension files, published assets, and the install record.",
"parameters": [],
"responses": {
"204": {
"description": "Extension removed."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# List users
Returns a paginated list of panel users, sorted with root administrators first by default.
## GET /api/admin/users
```json
{
"summary": "List users",
"operationId": "adminListUsers",
"description": "Returns a paginated list of panel users, sorted with root administrators first by default.",
"parameters": [
{
"in": "query",
"name": "page",
"description": "Page number to retrieve.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListUsersPageQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Results to return per page.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListUsersPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
},
{
"in": "query",
"name": "filter[email]",
"description": "Filter users by email address.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListUsersFilterEmailQueryParameter"
},
"examples": {
"default": {
"value": "admin@example.com"
}
}
},
{
"in": "query",
"name": "filter[uuid]",
"description": "Filter users by UUID.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListUsersFilterUuidQueryParameter"
},
"examples": {
"default": {
"value": "1b19cf3f-2f89-4f88-a81e-321e7fe326bc"
}
}
},
{
"in": "query",
"name": "filter[username]",
"description": "Filter users by username.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListUsersFilterUsernameQueryParameter"
},
"examples": {
"default": {
"value": "admin"
}
}
},
{
"in": "query",
"name": "filter[external_id]",
"description": "Filter users by external identifier.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListUsersFilterExternalIdQueryParameter"
},
"examples": {
"default": {
"value": "remote-123"
}
}
},
{
"in": "query",
"name": "filter[search]",
"description": "Search users by username, email, or UUID.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListUsersFilterSearchQueryParameter"
},
"examples": {
"default": {
"value": "admin"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort users by id, uuid, email, username, or creation date. Prefix with a hyphen for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListUsersSortQueryParameter"
},
"examples": {
"default": {
"value": "-created_at"
}
}
}
],
"responses": {
"200": {
"description": "Users returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUserPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Create user
Creates a panel user. Optional password and language fields may be supplied explicitly.
## POST /api/admin/users
```json
{
"summary": "Create user",
"operationId": "adminCreateUser",
"description": "Creates a panel user. Optional password and language fields may be supplied explicitly.",
"parameters": [],
"responses": {
"201": {
"description": "User created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUserResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminCreateUserRequest"
}
}
}
}
}
```
# Get user by external ID
Returns a single panel user by external identifier.
## GET /api/admin/users/external/{external_id}
```json
{
"summary": "Get user by external ID",
"operationId": "adminGetUserByExternalID",
"description": "Returns a single panel user by external identifier.",
"parameters": [],
"responses": {
"200": {
"description": "User returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUserResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Get user
Returns a single panel user by internal numeric ID.
## GET /api/admin/users/{user_id}
```json
{
"summary": "Get user",
"operationId": "adminGetUser",
"description": "Returns a single panel user by internal numeric ID.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports servers.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminGetUserIncludeQueryParameter"
},
"examples": {
"default": {
"value": "servers"
}
}
}
],
"responses": {
"200": {
"description": "User returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUserResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Delete user
Deletes a panel user. The authenticated administrator cannot delete their own account.
## DELETE /api/admin/users/{user_id}
```json
{
"summary": "Delete user",
"operationId": "adminDeleteUser",
"description": "Deletes a panel user. The authenticated administrator cannot delete their own account.",
"parameters": [],
"responses": {
"204": {
"description": "User deleted."
},
"400": {
"description": "The authenticated administrator attempted to delete their own user account.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Disable user two-factor authentication
Clears the configured TOTP secret and disables two-factor authentication for a user.
## POST /api/admin/users/{user_id}/disable-2fa
```json
{
"summary": "Disable user two-factor authentication",
"operationId": "adminDisableUserTwoFactorAuthentication",
"description": "Clears the configured TOTP secret and disables two-factor authentication for a user.",
"parameters": [],
"responses": {
"204": {
"description": "Two-factor authentication disabled."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Update user
Updates an existing panel user. Supplying a password changes the user password.
## PUT /api/admin/users/{id}
```json
{
"summary": "Update user",
"operationId": "adminUpdateUser",
"description": "Updates an existing panel user. Supplying a password changes the user password.",
"parameters": [],
"responses": {
"200": {
"description": "User updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUserResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateUserRequest"
}
}
}
}
}
```
# Get node system information
Proxies Wings system information for a node.
## GET /api/admin/nodes/{node_id}/system-information
```json
{
"summary": "Get node system information",
"operationId": "adminGetNodeSystemInformation",
"description": "Proxies Wings system information for a node.",
"parameters": [],
"responses": {
"200": {
"description": "System information returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminNodeSystemInformationResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"502": {
"description": "The panel could not connect to Wings.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Create node deploy token
Creates or reuses an application API key that can deploy the specified node.
## POST /api/admin/nodes/{node_id}/deploy-token
```json
{
"summary": "Create node deploy token",
"operationId": "adminCreateNodeDeployToken",
"description": "Creates or reuses an application API key that can deploy the specified node.",
"parameters": [],
"responses": {
"200": {
"description": "Deploy token returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminNodeDeployTokenResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Get node utilization
Returns aggregate resource utilization for a node.
## GET /api/admin/nodes/{node_id}/utilization
```json
{
"summary": "Get node utilization",
"operationId": "adminGetNodeUtilization",
"description": "Returns aggregate resource utilization for a node.",
"parameters": [],
"responses": {
"200": {
"description": "Utilization returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminNodeUtilizationResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# List server mounts
Returns mount definitions that are eligible for a server, including whether each mount is attached.
## GET /api/admin/servers/{server_admin_identifier}/mounts
```json
{
"summary": "List server mounts",
"operationId": "adminListServerMounts",
"description": "Returns mount definitions that are eligible for a server, including whether each mount is attached.",
"parameters": [],
"responses": {
"200": {
"description": "Eligible server mounts returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminMountListResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The server is not installed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Attach server mount
Attaches an eligible mount definition to a server.
## POST /api/admin/servers/{server_admin_identifier}/mounts
```json
{
"summary": "Attach server mount",
"operationId": "adminAttachServerMount",
"description": "Attaches an eligible mount definition to a server.",
"parameters": [],
"responses": {
"204": {
"description": "Mount attached."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The server is not installed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminAttachServerMountRequest"
}
}
}
}
}
```
# Detach server mount
Detaches a mount definition from a server.
## DELETE /api/admin/servers/{server_admin_identifier}/mounts/{mount_id}
```json
{
"summary": "Detach server mount",
"operationId": "adminDetachServerMount",
"description": "Detaches a mount definition from a server.",
"parameters": [],
"responses": {
"204": {
"description": "Mount detached."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The server is not installed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# List servers
Returns a paginated list of servers.
## GET /api/admin/servers
```json
{
"summary": "List servers",
"operationId": "adminListServers",
"description": "Returns a paginated list of servers.",
"parameters": [
{
"in": "query",
"name": "filter[*]",
"description": "Search servers by UUID, name, owner, node, allocation, or external identifier.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListServersFilterWildcardQueryParameter"
},
"examples": {
"default": {
"value": "Survival"
}
}
},
{
"in": "query",
"name": "filter[uuid]",
"description": "Filter servers by UUID.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListServersFilterUuidQueryParameter"
},
"examples": {
"default": {
"value": "1b19cf3f-2f89-4f88-a81e-321e7fe326bc"
}
}
},
{
"in": "query",
"name": "filter[uuidShort]",
"description": "Filter servers by short UUID identifier.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListServersFilterUuidShortQueryParameter"
},
"examples": {
"default": {
"value": "1b19cf3f"
}
}
},
{
"in": "query",
"name": "filter[name]",
"description": "Filter servers by name.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListServersFilterNameQueryParameter"
},
"examples": {
"default": {
"value": "Survival"
}
}
},
{
"in": "query",
"name": "filter[external_id]",
"description": "Filter servers by external identifier.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListServersFilterExternalIdQueryParameter"
},
"examples": {
"default": {
"value": "remote-123"
}
}
},
{
"in": "query",
"name": "filter[image]",
"description": "Filter servers by Docker image.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListServersFilterImageQueryParameter"
},
"examples": {
"default": {
"value": "ghcr.io/pterodactyl/yolks:java_21"
}
}
},
{
"in": "query",
"name": "filter[node_id]",
"description": "Filter servers by node ID.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListServersFilterNodeIdQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "filter[owner_id]",
"description": "Filter servers by owner user ID.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListServersFilterOwnerIdQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort servers by id, uuid, name, or creation time. Prefix with a hyphen for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListServersSortQueryParameter"
},
"examples": {
"default": {
"value": "-created_at"
}
}
},
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports allocation, allocations, user, subusers, egg, variables, location, node, and databases.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListServersIncludeQueryParameter"
},
"examples": {
"default": {
"value": "user,node,allocation"
}
}
},
{
"in": "query",
"name": "page",
"description": "Page number to retrieve.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListServersPageQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Results to return per page.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListServersPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
}
],
"responses": {
"200": {
"description": "Servers returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminServerPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Create server
Creates a server using explicit allocations or automatic deployment constraints.
## POST /api/admin/servers
```json
{
"summary": "Create server",
"operationId": "adminCreateServer",
"description": "Creates a server using explicit allocations or automatic deployment constraints.",
"parameters": [],
"responses": {
"201": {
"description": "Server created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminServerResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminCreateServerRequest"
}
}
}
}
}
```
# Get server by external ID
Returns a single server by external identifier.
## GET /api/admin/servers/external/{external_id}
```json
{
"summary": "Get server by external ID",
"operationId": "adminGetServerByExternalID",
"description": "Returns a single server by external identifier.",
"parameters": [],
"responses": {
"200": {
"description": "Server returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminServerResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Get server
Returns a single server by admin identifier.
## GET /api/admin/servers/{server_admin_identifier}
```json
{
"summary": "Get server",
"operationId": "adminGetServer",
"description": "Returns a single server by admin identifier.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports allocation, allocations, user, subusers, egg, variables, location, node, and databases.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminGetServerIncludeQueryParameter"
},
"examples": {
"default": {
"value": "allocations,user,node,egg,variables"
}
}
}
],
"responses": {
"200": {
"description": "Server returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminServerResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Delete server
Deletes a server and returns an error if Wings or database host cleanup fails.
## DELETE /api/admin/servers/{server_admin_identifier}
```json
{
"summary": "Delete server",
"operationId": "adminDeleteServer",
"description": "Deletes a server and returns an error if Wings or database host cleanup fails.",
"parameters": [],
"responses": {
"204": {
"description": "Server deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Update server details
Updates a server name, owner, description, and external identifier.
## PUT /api/admin/servers/{server_admin_identifier}/details
```json
{
"summary": "Update server details",
"operationId": "adminUpdateServerDetails",
"description": "Updates a server name, owner, description, and external identifier.",
"parameters": [],
"responses": {
"200": {
"description": "Server updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminServerResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateServerDetailsRequest"
}
}
}
}
}
```
# Update server build
Updates server limits, primary allocation, and feature limits.
## PUT /api/admin/servers/{server_admin_identifier}/build
```json
{
"summary": "Update server build",
"operationId": "adminUpdateServerBuild",
"description": "Updates server limits, primary allocation, and feature limits.",
"parameters": [],
"responses": {
"200": {
"description": "Server build updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminServerResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateServerBuildRequest"
}
}
}
}
}
```
# Update server startup
Updates the egg, Docker image, startup command, environment, and install-script behavior.
## PUT /api/admin/servers/{server_admin_identifier}/startup
```json
{
"summary": "Update server startup",
"operationId": "adminUpdateServerStartup",
"description": "Updates the egg, Docker image, startup command, environment, and install-script behavior.",
"parameters": [],
"responses": {
"200": {
"description": "Server startup updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminServerResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateServerStartupRequest"
}
}
}
}
}
```
# Update server suspension
Suspends or unsuspends a server.
## POST /api/admin/servers/{server_admin_identifier}/suspension
```json
{
"summary": "Update server suspension",
"operationId": "adminUpdateServerSuspension",
"description": "Suspends or unsuspends a server.",
"parameters": [],
"responses": {
"204": {
"description": "Server suspension state updated."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateServerSuspensionRequest"
}
}
}
}
}
```
# Reinstall server
Queues a server reinstall through Wings.
## POST /api/admin/servers/{server_admin_identifier}/reinstall
```json
{
"summary": "Reinstall server",
"operationId": "adminReinstallServer",
"description": "Queues a server reinstall through Wings.",
"parameters": [],
"responses": {
"202": {
"description": "Server reinstall accepted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Rebuild server
Synchronizes the server configuration to Wings.
## POST /api/admin/servers/{server_admin_identifier}/rebuild
```json
{
"summary": "Rebuild server",
"operationId": "adminRebuildServer",
"description": "Synchronizes the server configuration to Wings.",
"parameters": [],
"responses": {
"204": {
"description": "Server rebuild synchronized."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Get server transfer progress
Returns the active transfer for a server, or 204 when no transfer is in progress.
## GET /api/admin/servers/{server_admin_identifier}/transfer/progress
```json
{
"summary": "Get server transfer progress",
"operationId": "adminGetServerTransferProgress",
"description": "Returns the active transfer for a server, or 204 when no transfer is in progress.",
"parameters": [],
"responses": {
"200": {
"description": "Transfer progress returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminServerTransferResource"
}
}
}
},
"204": {
"description": "No server transfer is currently in progress."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Transfer server
Starts a server transfer to another node using the specified target allocations.
## POST /api/admin/servers/{server_admin_identifier}/transfer
```json
{
"summary": "Transfer server",
"operationId": "adminTransferServer",
"description": "Starts a server transfer to another node using the specified target allocations.",
"parameters": [],
"responses": {
"204": {
"description": "Server transfer started."
},
"400": {
"description": "The destination node cannot fit the server or the server cannot currently be transferred.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminTransferServerRequest"
}
}
}
}
}
```
# Toggle server install state
Toggles a server between installed and installing states unless it is marked as failed.
## POST /api/admin/servers/{server_admin_identifier}/settings/toggle-install
```json
{
"summary": "Toggle server install state",
"operationId": "adminToggleServerInstallState",
"description": "Toggles a server between installed and installing states unless it is marked as failed.",
"parameters": [],
"responses": {
"204": {
"description": "Server install state toggled."
},
"400": {
"description": "The server is marked as failed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Force delete server
Deletes a server while bypassing recoverable Wings or database host cleanup failures.
## DELETE /api/admin/servers/{server_admin_identifier}/force
```json
{
"summary": "Force delete server",
"operationId": "adminForceDeleteServer",
"description": "Deletes a server while bypassing recoverable Wings or database host cleanup failures.",
"parameters": [],
"responses": {
"204": {
"description": "Server force deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# List server databases
Returns all databases assigned to a server.
## GET /api/admin/servers/{server_admin_identifier}/databases
```json
{
"summary": "List server databases",
"operationId": "adminListServerDatabases",
"description": "Returns all databases assigned to a server.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"password\".",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListServerDatabasesIncludeQueryParameter"
},
"examples": {
"default": {
"value": "password"
}
}
}
],
"responses": {
"200": {
"description": "Server databases returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminServerDatabaseListResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The server is not installed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Create server database
Creates a database for a server and returns the generated password.
## POST /api/admin/servers/{server_admin_identifier}/databases
```json
{
"summary": "Create server database",
"operationId": "adminCreateServerDatabase",
"description": "Creates a database for a server and returns the generated password.",
"parameters": [],
"responses": {
"201": {
"description": "Server database created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminServerDatabaseResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The server is not installed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminCreateServerDatabaseRequest"
}
}
}
}
}
```
# Get server database
Returns a single database assigned to a server.
## GET /api/admin/servers/{server_admin_identifier}/databases/{database_id}
```json
{
"summary": "Get server database",
"operationId": "adminGetServerDatabase",
"description": "Returns a single database assigned to a server.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"password\".",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminGetServerDatabaseIncludeQueryParameter"
},
"examples": {
"default": {
"value": "password"
}
}
}
],
"responses": {
"200": {
"description": "Server database returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminServerDatabaseResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The server is not installed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Delete server database
Deletes a database assigned to a server.
## DELETE /api/admin/servers/{server_admin_identifier}/databases/{database_id}
```json
{
"summary": "Delete server database",
"operationId": "adminDeleteServerDatabase",
"description": "Deletes a database assigned to a server.",
"parameters": [],
"responses": {
"204": {
"description": "Server database deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The server is not installed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Rotate server database password
Rotates the password for a database assigned to a server.
## POST /api/admin/servers/{server_admin_identifier}/databases/{database_id}/rotate-password
```json
{
"summary": "Rotate server database password",
"operationId": "adminRotateServerDatabasePassword",
"description": "Rotates the password for a database assigned to a server.",
"parameters": [],
"responses": {
"200": {
"description": "Database password rotated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminServerDatabaseResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# List server backups
Returns a paginated list of backups assigned to a server.
## GET /api/admin/servers/{server_admin_identifier}/backups
```json
{
"summary": "List server backups",
"operationId": "adminListServerBackups",
"description": "Returns a paginated list of backups assigned to a server.",
"parameters": [
{
"in": "query",
"name": "filter[uuid]",
"description": "Filter backups by UUID.",
"required": true,
"schema": {
"$ref": "#/components/schemas/AdminListServerBackupsFilterUuidQueryParameter"
},
"examples": {
"default": {
"value": "1b19cf3f-2f89-4f88-a81e-321e7fe326bc"
}
}
},
{
"in": "query",
"name": "filter[name]",
"description": "Filter backups by name.",
"required": true,
"schema": {
"$ref": "#/components/schemas/AdminListServerBackupsFilterNameQueryParameter"
},
"examples": {
"default": {
"value": "Before Update"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort backups by id, bytes, or created_at. Prefix with a hyphen for descending order.",
"required": true,
"schema": {
"$ref": "#/components/schemas/AdminListServerBackupsSortQueryParameter"
},
"examples": {
"default": {
"value": "-created_at"
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Results to return per page.",
"required": true,
"schema": {
"$ref": "#/components/schemas/AdminListServerBackupsPerPageQueryParameter"
},
"examples": {
"default": {
"value": 25
}
}
}
],
"responses": {
"200": {
"description": "Server backups returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminBackupPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Toggle server backup lock
Toggles whether a server backup is locked against deletion.
## POST /api/admin/servers/{server_admin_identifier}/backups/{backup_id}/toggle
```json
{
"summary": "Toggle server backup lock",
"operationId": "adminToggleServerBackupLock",
"description": "Toggles whether a server backup is locked against deletion.",
"parameters": [],
"responses": {
"200": {
"description": "Server backup lock state toggled.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminBackupResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# List nodes
Returns a paginated list of Wings nodes.
## GET /api/admin/nodes
```json
{
"summary": "List nodes",
"operationId": "adminListNodes",
"description": "Returns a paginated list of Wings nodes.",
"parameters": [
{
"in": "query",
"name": "filter[uuid]",
"description": "Filter nodes by UUID.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListNodesFilterUuidQueryParameter"
},
"examples": {
"default": {
"value": "1b19cf3f-2f89-4f88-a81e-321e7fe326bc"
}
}
},
{
"in": "query",
"name": "filter[name]",
"description": "Filter nodes by name.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListNodesFilterNameQueryParameter"
},
"examples": {
"default": {
"value": "node-1"
}
}
},
{
"in": "query",
"name": "filter[fqdn]",
"description": "Filter nodes by FQDN.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListNodesFilterFqdnQueryParameter"
},
"examples": {
"default": {
"value": "node.example.com"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort nodes by id, uuid, name, memory, disk, or creation time. Prefix with a hyphen for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListNodesSortQueryParameter"
},
"examples": {
"default": {
"value": "-created_at"
}
}
},
{
"in": "query",
"name": "page",
"description": "Page number to return.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListNodesPageQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Results to return per page.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListNodesPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
},
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"allocations\", \"location\", and \"servers\".",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListNodesIncludeQueryParameter"
},
"examples": {
"default": {
"value": "location"
}
}
}
],
"responses": {
"200": {
"description": "Nodes returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminNodePaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Create node
Creates a Wings node. HTTPS nodes must use a valid FQDN rather than a raw IP address.
## POST /api/admin/nodes
```json
{
"summary": "Create node",
"operationId": "adminCreateNode",
"description": "Creates a Wings node. HTTPS nodes must use a valid FQDN rather than a raw IP address.",
"parameters": [],
"responses": {
"201": {
"description": "Node created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminNodeResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminCreateNodeRequest"
}
}
}
}
}
```
# Get node
Returns a single Wings node by ID.
## GET /api/admin/nodes/{node_id}
```json
{
"summary": "Get node",
"operationId": "adminGetNode",
"description": "Returns a single Wings node by ID.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"allocations\", \"location\", and \"servers\".",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminGetNodeIncludeQueryParameter"
},
"examples": {
"default": {
"value": "location"
}
}
}
],
"responses": {
"200": {
"description": "Node returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminNodeResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Delete node
Deletes a node when it has no active servers.
## DELETE /api/admin/nodes/{node_id}
```json
{
"summary": "Delete node",
"operationId": "adminDeleteNode",
"description": "Deletes a node when it has no active servers.",
"parameters": [],
"responses": {
"204": {
"description": "Node deleted."
},
"400": {
"description": "The node has active servers.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Get node configuration
Returns the raw Wings configuration payload for a node, including daemon authentication tokens.
## GET /api/admin/nodes/{node_id}/configuration
```json
{
"summary": "Get node configuration",
"operationId": "adminGetNodeConfiguration",
"description": "Returns the raw Wings configuration payload for a node, including daemon authentication tokens.",
"parameters": [],
"responses": {
"200": {
"description": "Node configuration returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminNodeConfigurationResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Update node
Updates a Wings node and optionally rotates its daemon secret.
## PUT /api/admin/nodes/{id}
```json
{
"summary": "Update node",
"operationId": "adminUpdateNode",
"description": "Updates a Wings node and optionally rotates its daemon secret.",
"parameters": [],
"responses": {
"200": {
"description": "Node updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminNodeResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateNodeRequest"
}
}
}
}
}
```
# Sync node game tags
Replaces the games a node accepts. A node with no game tags accepts any egg.
## PUT /api/admin/nodes/{node_id}/tags
```json
{
"summary": "Sync node game tags",
"operationId": "adminSyncNodeGameTags",
"description": "Replaces the games a node accepts. A node with no game tags accepts any egg.",
"parameters": [],
"responses": {
"200": {
"description": "Games the node accepts.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminTagListResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminSyncNodeGameTagsRequest"
}
}
}
}
}
```
# Sync node deployment tags
Replaces the reservations on a node. A deploy must declare exactly these tags to land here.
## PUT /api/admin/nodes/{node_id}/deployment-tags
```json
{
"summary": "Sync node deployment tags",
"operationId": "adminSyncNodeDeploymentTags",
"description": "Replaces the reservations on a node. A deploy must declare exactly these tags to land here.",
"parameters": [],
"responses": {
"200": {
"description": "Reservations on the node.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminTagListResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminSyncNodeDeploymentTagsRequest"
}
}
}
}
}
```
# List node allocations
Returns a paginated list of allocations for a node.
## GET /api/admin/nodes/{node_id}/allocations
```json
{
"summary": "List node allocations",
"operationId": "adminListNodeAllocations",
"description": "Returns a paginated list of allocations for a node.",
"parameters": [
{
"in": "query",
"name": "filter[ip]",
"description": "Filter allocations by exact IP address.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListNodeAllocationsFilterIpQueryParameter"
},
"examples": {
"default": {
"value": "192.168.1.1"
}
}
},
{
"in": "query",
"name": "filter[port]",
"description": "Filter allocations by exact port.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListNodeAllocationsFilterPortQueryParameter"
},
"examples": {
"default": {
"value": 25565
}
}
},
{
"in": "query",
"name": "filter[ip_alias]",
"description": "Filter allocations by IP alias.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListNodeAllocationsFilterIpAliasQueryParameter"
},
"examples": {
"default": {
"value": "game.example.com"
}
}
},
{
"in": "query",
"name": "filter[server_id]",
"description": "Filter allocations by server ID, or pass an empty value to return unassigned allocations.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListNodeAllocationsFilterServerIdQueryParameter"
},
"examples": {
"default": {
"value": "42"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort allocations by id, ip, or port. Prefix with a hyphen for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListNodeAllocationsSortQueryParameter"
},
"examples": {
"default": {
"value": "port"
}
}
},
{
"in": "query",
"name": "page",
"description": "Page number to return.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListNodeAllocationsPageQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Results to return per page.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListNodeAllocationsPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
}
],
"responses": {
"200": {
"description": "Allocations returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminAllocationPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Create node allocations
Creates one or more allocations on a node from IP addresses and ports or port ranges.
## POST /api/admin/nodes/{node_id}/allocations
```json
{
"summary": "Create node allocations",
"operationId": "adminCreateNodeAllocations",
"description": "Creates one or more allocations on a node from IP addresses and ports or port ranges.",
"parameters": [],
"responses": {
"204": {
"description": "Allocations created."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminCreateNodeAllocationsRequest"
}
}
}
}
}
```
# Bulk delete node allocations
Deletes multiple unassigned allocations from a node by allocation ID.
## DELETE /api/admin/nodes/{node_id}/allocations
```json
{
"summary": "Bulk delete node allocations",
"operationId": "adminBulkDeleteNodeAllocations",
"description": "Deletes multiple unassigned allocations from a node by allocation ID.",
"parameters": [],
"responses": {
"204": {
"description": "Allocations deleted."
},
"400": {
"description": "One of the allocations is currently assigned to a server.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminBulkDeleteNodeAllocationsRequest"
}
}
}
}
}
```
# List node allocation IPs
Returns distinct allocation IP addresses for a node.
## GET /api/admin/nodes/{node_id}/allocations/ips
```json
{
"summary": "List node allocation IPs",
"operationId": "adminListNodeAllocationIPs",
"description": "Returns distinct allocation IP addresses for a node.",
"parameters": [],
"responses": {
"200": {
"description": "Allocation IPs returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminListNodeAllocationIPsResponseBody"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# List available node allocations
Returns unassigned allocations for a node.
## GET /api/admin/nodes/{node_id}/allocations/available
```json
{
"summary": "List available node allocations",
"operationId": "adminListAvailableNodeAllocations",
"description": "Returns unassigned allocations for a node.",
"parameters": [
{
"in": "query",
"name": "filter[ip]",
"description": "Filter allocations by exact IP address.",
"required": true,
"schema": {
"$ref": "#/components/schemas/AdminListAvailableNodeAllocationsFilterIpQueryParameter"
},
"examples": {
"default": {
"value": "192.168.1.1"
}
}
},
{
"in": "query",
"name": "filter[port]",
"description": "Filter allocations by exact port.",
"required": true,
"schema": {
"$ref": "#/components/schemas/AdminListAvailableNodeAllocationsFilterPortQueryParameter"
},
"examples": {
"default": {
"value": 25565
}
}
},
{
"in": "query",
"name": "filter[ip_alias]",
"description": "Filter allocations by IP alias.",
"required": true,
"schema": {
"$ref": "#/components/schemas/AdminListAvailableNodeAllocationsFilterIpAliasQueryParameter"
},
"examples": {
"default": {
"value": "game.example.com"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort allocations by id, ip, or port. Prefix with a hyphen for descending order.",
"required": true,
"schema": {
"$ref": "#/components/schemas/AdminListAvailableNodeAllocationsSortQueryParameter"
},
"examples": {
"default": {
"value": "port"
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Results to return per page.",
"required": true,
"schema": {
"$ref": "#/components/schemas/AdminListAvailableNodeAllocationsPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
}
],
"responses": {
"200": {
"description": "Available allocations returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminAllocationPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Delete node allocation IP block
Deletes every unassigned allocation for an IP address on a node.
## DELETE /api/admin/nodes/{node_id}/allocations/block
```json
{
"summary": "Delete node allocation IP block",
"operationId": "adminDeleteNodeAllocationIPBlock",
"description": "Deletes every unassigned allocation for an IP address on a node.",
"parameters": [],
"responses": {
"204": {
"description": "Unassigned allocations for the IP were deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminDeleteNodeAllocationIPBlockRequest"
}
}
}
}
}
```
# Update node allocation
Updates the alias for a single allocation on a node.
## PUT /api/admin/nodes/{node_id}/allocations/{id}
```json
{
"summary": "Update node allocation",
"operationId": "adminUpdateNodeAllocation",
"description": "Updates the alias for a single allocation on a node.",
"parameters": [],
"responses": {
"200": {
"description": "Allocation updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminAllocationResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateNodeAllocationRequest"
}
}
}
}
}
```
# Delete node allocation
Deletes a single unassigned allocation from a node.
## DELETE /api/admin/nodes/{node_id}/allocations/{allocation_id}
```json
{
"summary": "Delete node allocation",
"operationId": "adminDeleteNodeAllocation",
"description": "Deletes a single unassigned allocation from a node.",
"parameters": [],
"responses": {
"204": {
"description": "Allocation deleted."
},
"400": {
"description": "The allocation is currently assigned to a server.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# List locations
Returns a paginated list of locations.
## GET /api/admin/locations
```json
{
"summary": "List locations",
"operationId": "adminListLocations",
"description": "Returns a paginated list of locations.",
"parameters": [
{
"in": "query",
"name": "filter[short]",
"description": "Filter locations by short identifier.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListLocationsFilterShortQueryParameter"
},
"examples": {
"default": {
"value": "us-east"
}
}
},
{
"in": "query",
"name": "filter[long]",
"description": "Filter locations by long description.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListLocationsFilterLongQueryParameter"
},
"examples": {
"default": {
"value": "US East"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort locations by ID, short identifier, or creation date. Prefix with a hyphen for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListLocationsSortQueryParameter"
},
"examples": {
"default": {
"value": "short"
}
}
},
{
"in": "query",
"name": "page",
"description": "Page number to return.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListLocationsPageQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Results to return per page.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListLocationsPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
}
],
"responses": {
"200": {
"description": "Locations returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminLocationPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Create location
Creates a location for grouping nodes and servers.
## POST /api/admin/locations
```json
{
"summary": "Create location",
"operationId": "adminCreateLocation",
"description": "Creates a location for grouping nodes and servers.",
"parameters": [],
"responses": {
"201": {
"description": "Location created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminLocationResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminCreateLocationRequest"
}
}
}
}
}
```
# Get location
Returns a single location by ID.
## GET /api/admin/locations/{location_id}
```json
{
"summary": "Get location",
"operationId": "adminGetLocation",
"description": "Returns a single location by ID.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"nodes\" and \"servers\".",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminGetLocationIncludeQueryParameter"
},
"examples": {
"default": {
"value": "nodes"
}
}
}
],
"responses": {
"200": {
"description": "Location returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminLocationResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Delete location
Deletes a location when no dependent resources prevent removal.
## DELETE /api/admin/locations/{location_id}
```json
{
"summary": "Delete location",
"operationId": "adminDeleteLocation",
"description": "Deletes a location when no dependent resources prevent removal.",
"parameters": [],
"responses": {
"204": {
"description": "Location deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# List eligible location nodes
Returns nodes assigned to a location that can be selected for location-scoped workflows.
## GET /api/admin/locations/{location_id}/eligible-nodes
```json
{
"summary": "List eligible location nodes",
"operationId": "adminListEligibleLocationNodes",
"description": "Returns nodes assigned to a location that can be selected for location-scoped workflows.",
"parameters": [],
"responses": {
"200": {
"description": "Eligible nodes returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminNodeListResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Update location
Updates a location short identifier and description.
## PUT /api/admin/locations/{id}
```json
{
"summary": "Update location",
"operationId": "adminUpdateLocation",
"description": "Updates a location short identifier and description.",
"parameters": [],
"responses": {
"200": {
"description": "Location updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminLocationResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateLocationRequest"
}
}
}
}
}
```
# List mounts
Returns a paginated list of mount definitions.
## GET /api/admin/mounts
```json
{
"summary": "List mounts",
"operationId": "adminListMounts",
"description": "Returns a paginated list of mount definitions.",
"parameters": [
{
"in": "query",
"name": "filter[name]",
"description": "Filter mounts by name.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListMountsFilterNameQueryParameter"
},
"examples": {
"default": {
"value": "Shared Mods"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort mounts by id or name. Prefix with a hyphen for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListMountsSortQueryParameter"
},
"examples": {
"default": {
"value": "name"
}
}
},
{
"in": "query",
"name": "page",
"description": "Page number to return.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListMountsPageQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Results to return per page.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListMountsPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
},
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"eggs\", \"nodes\", and \"servers\".",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListMountsIncludeQueryParameter"
},
"examples": {
"default": {
"value": "eggs,nodes"
}
}
}
],
"responses": {
"200": {
"description": "Mounts returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminMountPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Create mount
Creates a mount definition and generates its UUID.
## POST /api/admin/mounts
```json
{
"summary": "Create mount",
"operationId": "adminCreateMount",
"description": "Creates a mount definition and generates its UUID.",
"parameters": [],
"responses": {
"201": {
"description": "Mount created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminMountResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminCreateMountRequest"
}
}
}
}
}
```
# Get mount
Returns a single mount definition by ID.
## GET /api/admin/mounts/{mount_id}
```json
{
"summary": "Get mount",
"operationId": "adminGetMount",
"description": "Returns a single mount definition by ID.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"eggs\", \"nodes\", and \"servers\".",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminGetMountIncludeQueryParameter"
},
"examples": {
"default": {
"value": "eggs,nodes"
}
}
}
],
"responses": {
"200": {
"description": "Mount returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminMountResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Delete mount
Deletes a mount definition.
## DELETE /api/admin/mounts/{mount_id}
```json
{
"summary": "Delete mount",
"operationId": "adminDeleteMount",
"description": "Deletes a mount definition.",
"parameters": [],
"responses": {
"204": {
"description": "Mount deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Update mount
Updates an existing mount definition.
## PUT /api/admin/mounts/{id}
```json
{
"summary": "Update mount",
"operationId": "adminUpdateMount",
"description": "Updates an existing mount definition.",
"parameters": [],
"responses": {
"200": {
"description": "Mount updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminMountResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateMountRequest"
}
}
}
}
}
```
# Attach eggs to mount
Attaches one or more eggs to a mount definition without removing existing egg attachments.
## POST /api/admin/mounts/{mount_id}/eggs
```json
{
"summary": "Attach eggs to mount",
"operationId": "adminAttachEggsToMount",
"description": "Attaches one or more eggs to a mount definition without removing existing egg attachments.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"eggs\", \"nodes\", and \"servers\".",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminAttachEggsToMountIncludeQueryParameter"
},
"examples": {
"default": {
"value": "eggs,nodes"
}
}
}
],
"responses": {
"200": {
"description": "Mount updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminMountResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminAttachEggsToMountRequest"
}
}
}
}
}
```
# Detach egg from mount
Removes a single egg attachment from a mount definition.
## DELETE /api/admin/mounts/{mount_id}/eggs/{egg_id}
```json
{
"summary": "Detach egg from mount",
"operationId": "adminDetachEggFromMount",
"description": "Removes a single egg attachment from a mount definition.",
"parameters": [],
"responses": {
"204": {
"description": "Egg detached from mount."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Attach nodes to mount
Attaches one or more nodes to a mount definition without removing existing node attachments.
## POST /api/admin/mounts/{mount_id}/nodes
```json
{
"summary": "Attach nodes to mount",
"operationId": "adminAttachNodesToMount",
"description": "Attaches one or more nodes to a mount definition without removing existing node attachments.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"eggs\", \"nodes\", and \"servers\".",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminAttachNodesToMountIncludeQueryParameter"
},
"examples": {
"default": {
"value": "eggs,nodes"
}
}
}
],
"responses": {
"200": {
"description": "Mount updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminMountResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminAttachNodesToMountRequest"
}
}
}
}
}
```
# Detach node from mount
Removes a single node attachment from a mount definition.
## DELETE /api/admin/mounts/{mount_id}/nodes/{node_id}
```json
{
"summary": "Detach node from mount",
"operationId": "adminDetachNodeFromMount",
"description": "Removes a single node attachment from a mount definition.",
"parameters": [],
"responses": {
"204": {
"description": "Node detached from mount."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# List egg catalog
Returns the cached Pterodactyl egg catalog for searching and filtering. The index is cached for one hour.
## GET /api/admin/eggs/catalog
```json
{
"summary": "List egg catalog",
"operationId": "adminListEggCatalog",
"description": "Returns the cached Pterodactyl egg catalog for searching and filtering. The index is cached for one hour.",
"parameters": [],
"responses": {
"200": {
"description": "Catalog entries returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminListEggCatalogResponseBody"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"502": {
"description": "The upstream catalog is unavailable or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Refresh egg catalog
Clears the cached Pterodactyl catalog so the next listing downloads the current index.
## POST /api/admin/eggs/catalog/refresh
```json
{
"summary": "Refresh egg catalog",
"operationId": "adminRefreshEggCatalog",
"description": "Clears the cached Pterodactyl catalog so the next listing downloads the current index.",
"parameters": [],
"responses": {
"204": {
"description": "Catalog cache cleared."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Import catalog egg
Downloads the selected entry from the Pterodactyl catalog and imports its egg definition and variables.
## POST /api/admin/eggs/catalog/import
```json
{
"summary": "Import catalog egg",
"operationId": "adminImportCatalogEgg",
"description": "Downloads the selected entry from the Pterodactyl catalog and imports its egg definition and variables.",
"parameters": [],
"responses": {
"201": {
"description": "Egg imported.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminEggResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The selected catalog entry does not exist.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
},
"502": {
"description": "The upstream catalog or egg definition could not be loaded.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminImportCatalogEggRequest"
}
}
}
}
}
```
# List egg variables
Returns variables for an egg in display order.
## GET /api/admin/eggs/{egg_id}/variables
```json
{
"summary": "List egg variables",
"operationId": "adminListEggVariables",
"description": "Returns variables for an egg in display order.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Unknown includes are ignored.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListEggVariablesIncludeQueryParameter"
},
"examples": {
"default": {
"value": "egg"
}
}
}
],
"responses": {
"200": {
"description": "Egg variables returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminEggVariableListResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Create egg variable
Creates a startup variable for an egg.
## POST /api/admin/eggs/{egg_id}/variables
```json
{
"summary": "Create egg variable",
"operationId": "adminCreateEggVariable",
"description": "Creates a startup variable for an egg.",
"parameters": [],
"responses": {
"201": {
"description": "Egg variable created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminEggVariableResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminCreateEggVariableRequest"
}
}
}
}
}
```
# Reorder egg variables
Updates the display order for variables on an egg.
## PUT /api/admin/eggs/{egg_id}/variables/reorder
```json
{
"summary": "Reorder egg variables",
"operationId": "adminReorderEggVariables",
"description": "Updates the display order for variables on an egg.",
"parameters": [],
"responses": {
"200": {
"description": "Egg variables reordered.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminEggVariableListResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminReorderEggVariablesRequest"
}
}
}
}
}
```
# Update egg variable
Updates a startup variable for an egg.
## PUT /api/admin/eggs/{egg_id}/variables/{id}
```json
{
"summary": "Update egg variable",
"operationId": "adminUpdateEggVariable",
"description": "Updates a startup variable for an egg.",
"parameters": [],
"responses": {
"200": {
"description": "Egg variable updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminEggVariableResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateEggVariableRequest"
}
}
}
}
}
```
# Delete egg variable
Deletes a startup variable from an egg.
## DELETE /api/admin/eggs/{egg_id}/variables/{variable_id}
```json
{
"summary": "Delete egg variable",
"operationId": "adminDeleteEggVariable",
"description": "Deletes a startup variable from an egg.",
"parameters": [],
"responses": {
"204": {
"description": "Egg variable deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Get egg install script
Returns an egg with its install script block.
## GET /api/admin/eggs/{egg_id}/scripts
```json
{
"summary": "Get egg install script",
"operationId": "adminGetEggInstallScript",
"description": "Returns an egg with its install script block.",
"parameters": [],
"responses": {
"200": {
"description": "Egg install script returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminEggResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Update egg install script
Updates the install script configuration for an egg.
## PUT /api/admin/eggs/{egg_id}/scripts
```json
{
"summary": "Update egg install script",
"operationId": "adminUpdateEggInstallScript",
"description": "Updates the install script configuration for an egg.",
"parameters": [],
"responses": {
"200": {
"description": "Egg install script updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminEggResource"
}
}
}
},
"400": {
"description": "The selected copy source is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateEggInstallScriptRequest"
}
}
}
}
}
```
# Export egg
Downloads an egg share JSON file.
## GET /api/admin/eggs/{egg_id}/export
```json
{
"summary": "Export egg",
"operationId": "adminExportEgg",
"description": "Downloads an egg share JSON file.",
"parameters": [],
"responses": {
"200": {
"description": "Egg share file returned.",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/AdminExportEggResponseBody"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Update egg from import
Updates an existing egg from an uploaded egg share JSON file.
## POST /api/admin/eggs/{egg_id}/import
```json
{
"summary": "Update egg from import",
"operationId": "adminUpdateEggFromImport",
"description": "Updates an existing egg from an uploaded egg share JSON file.",
"parameters": [],
"responses": {
"200": {
"description": "Egg updated from import.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminEggResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateEggFromImportRequest"
}
}
}
}
}
```
# Sync egg tags
Replaces the tags applied to an egg. Values may be tag IDs or new tag names.
## PUT /api/admin/eggs/{egg_id}/tags
```json
{
"summary": "Sync egg tags",
"operationId": "adminSyncEggTags",
"description": "Replaces the tags applied to an egg. Values may be tag IDs or new tag names.",
"parameters": [],
"responses": {
"200": {
"description": "Tags applied to the egg.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminTagListResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminSyncEggTagsRequest"
}
}
}
}
}
```
# Import egg
Creates an egg from an uploaded egg share JSON file.
## POST /api/admin/eggs/import
```json
{
"summary": "Import egg",
"operationId": "adminImportEgg",
"description": "Creates an egg from an uploaded egg share JSON file.",
"parameters": [],
"responses": {
"201": {
"description": "Egg imported.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminEggResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/AdminImportEggRequest"
}
}
}
}
}
```
# List eggs
Returns a paginated list of egg definitions.
## GET /api/admin/eggs
```json
{
"summary": "List eggs",
"operationId": "adminListEggs",
"description": "Returns a paginated list of egg definitions.",
"parameters": [
{
"in": "query",
"name": "filter[name]",
"description": "Filter eggs by name.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListEggsFilterNameQueryParameter"
},
"examples": {
"default": {
"value": "Paper"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort eggs by id or name. Prefix with a hyphen for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListEggsSortQueryParameter"
},
"examples": {
"default": {
"value": "name"
}
}
},
{
"in": "query",
"name": "page",
"description": "Page number to return.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListEggsPageQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Results to return per page.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListEggsPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
},
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include: variables, tags, servers.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListEggsIncludeQueryParameter"
},
"examples": {
"default": {
"value": "variables"
}
}
}
],
"responses": {
"200": {
"description": "Eggs returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminEggPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Create egg
Creates an egg definition.
## POST /api/admin/eggs
```json
{
"summary": "Create egg",
"operationId": "adminCreateEgg",
"description": "Creates an egg definition.",
"parameters": [],
"responses": {
"201": {
"description": "Egg created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminEggResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminCreateEggRequest"
}
}
}
}
}
```
# Get egg
Returns a single egg definition by ID.
## GET /api/admin/eggs/{egg_id}
```json
{
"summary": "Get egg",
"operationId": "adminGetEgg",
"description": "Returns a single egg definition by ID.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include: variables, tags, servers.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminGetEggIncludeQueryParameter"
},
"examples": {
"default": {
"value": "variables,tags"
}
}
}
],
"responses": {
"200": {
"description": "Egg returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminEggResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Delete egg
Deletes an egg definition.
## DELETE /api/admin/eggs/{egg_id}
```json
{
"summary": "Delete egg",
"operationId": "adminDeleteEgg",
"description": "Deletes an egg definition.",
"parameters": [],
"responses": {
"204": {
"description": "Egg deleted."
},
"400": {
"description": "The egg has active servers or child eggs.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Update egg
Updates an egg definition.
## PUT /api/admin/eggs/{id}
```json
{
"summary": "Update egg",
"operationId": "adminUpdateEgg",
"description": "Updates an egg definition.",
"parameters": [],
"responses": {
"200": {
"description": "Egg updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminEggResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateEggRequest"
}
}
}
}
}
```
# List tags
Returns a paginated list of tags.
## GET /api/admin/tags
```json
{
"summary": "List tags",
"operationId": "adminListTags",
"description": "Returns a paginated list of tags.",
"parameters": [
{
"in": "query",
"name": "filter[name]",
"description": "Filter tags by name.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListTagsFilterNameQueryParameter"
},
"examples": {
"default": {
"value": "Minecraft"
}
}
},
{
"in": "query",
"name": "filter[slug]",
"description": "Filter tags by slug.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListTagsFilterSlugQueryParameter"
},
"examples": {
"default": {
"value": "minecraft"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort tags by id, name, or slug. Prefix with a hyphen for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListTagsSortQueryParameter"
},
"examples": {
"default": {
"value": "name"
}
}
},
{
"in": "query",
"name": "page",
"description": "Page number to return.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListTagsPageQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Results to return per page.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListTagsPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
},
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"eggs\" and \"nodes\".",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListTagsIncludeQueryParameter"
},
"examples": {
"default": {
"value": "eggs"
}
}
}
],
"responses": {
"200": {
"description": "Tags returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminTagPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Create tag
Creates a tag.
## POST /api/admin/tags
```json
{
"summary": "Create tag",
"operationId": "adminCreateTag",
"description": "Creates a tag.",
"parameters": [],
"responses": {
"201": {
"description": "Tag created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminTagResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminCreateTagRequest"
}
}
}
}
}
```
# Get tag
Returns a single tag by ID.
## GET /api/admin/tags/{tag_id}
```json
{
"summary": "Get tag",
"operationId": "adminGetTag",
"description": "Returns a single tag by ID.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"eggs\" and \"nodes\".",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminGetTagIncludeQueryParameter"
},
"examples": {
"default": {
"value": "eggs"
}
}
}
],
"responses": {
"200": {
"description": "Tag returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminTagResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Delete tag
Deletes a tag and detaches it from every egg and node.
## DELETE /api/admin/tags/{tag_id}
```json
{
"summary": "Delete tag",
"operationId": "adminDeleteTag",
"description": "Deletes a tag and detaches it from every egg and node.",
"parameters": [],
"responses": {
"204": {
"description": "Tag deleted."
},
"400": {
"description": "The tag is a built-in game tag.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Update tag
Updates an existing tag. Built-in game tags cannot be edited.
## PUT /api/admin/tags/{id}
```json
{
"summary": "Update tag",
"operationId": "adminUpdateTag",
"description": "Updates an existing tag. Built-in game tags cannot be edited.",
"parameters": [],
"responses": {
"200": {
"description": "Tag updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminTagResource"
}
}
}
},
"400": {
"description": "The tag is a built-in game tag.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateTagRequest"
}
}
}
}
}
```
# Get settings
Returns current general, mail, advanced, and metadata settings.
## GET /api/admin/settings
```json
{
"summary": "Get settings",
"operationId": "adminGetSettings",
"description": "Returns current general, mail, advanced, and metadata settings.",
"parameters": [],
"responses": {
"200": {
"description": "Settings returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminSettingsResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Update general settings
Updates panel branding, 2FA requirements, and default locale settings.
## PUT /api/admin/settings/general
```json
{
"summary": "Update general settings",
"operationId": "adminUpdateGeneralSettings",
"description": "Updates panel branding, 2FA requirements, and default locale settings.",
"parameters": [],
"responses": {
"204": {
"description": "General settings updated."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateGeneralSettingsRequest"
}
}
}
}
}
```
# Update mail settings
Updates SMTP mail settings and restarts queued workers so they reload configuration.
## PUT /api/admin/settings/mail
```json
{
"summary": "Update mail settings",
"operationId": "adminUpdateMailSettings",
"description": "Updates SMTP mail settings and restarts queued workers so they reload configuration.",
"parameters": [],
"responses": {
"204": {
"description": "Mail settings updated."
},
"400": {
"description": "The panel is not configured to use SMTP mail.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateMailSettingsRequest"
}
}
}
}
}
```
# Send test mail
Sends a test email to the authenticated administrator using the configured mail transport.
## POST /api/admin/settings/mail/test
```json
{
"summary": "Send test mail",
"operationId": "adminSendTestMail",
"description": "Sends a test email to the authenticated administrator using the configured mail transport.",
"parameters": [],
"responses": {
"204": {
"description": "Test mail queued."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"500": {
"description": "The configured mail transport failed to send the message.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Update advanced settings
Updates reCAPTCHA, HTTP client timeout, and client allocation feature settings.
## PUT /api/admin/settings/advanced
```json
{
"summary": "Update advanced settings",
"operationId": "adminUpdateAdvancedSettings",
"description": "Updates reCAPTCHA, HTTP client timeout, and client allocation feature settings.",
"parameters": [],
"responses": {
"204": {
"description": "Advanced settings updated."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateAdvancedSettingsRequest"
}
}
}
}
}
```
# List database hosts
Returns a paginated list of database hosts.
## GET /api/admin/database-hosts
```json
{
"summary": "List database hosts",
"operationId": "adminListDatabaseHosts",
"description": "Returns a paginated list of database hosts.",
"parameters": [
{
"in": "query",
"name": "page",
"description": "Page number to retrieve.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListDatabaseHostsPageQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Results to return per page.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListDatabaseHostsPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
},
{
"in": "query",
"name": "filter[name]",
"description": "Filter hosts by name.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListDatabaseHostsFilterNameQueryParameter"
},
"examples": {
"default": {
"value": "Primary"
}
}
},
{
"in": "query",
"name": "filter[host]",
"description": "Filter hosts by address or hostname.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListDatabaseHostsFilterHostQueryParameter"
},
"examples": {
"default": {
"value": "127.0.0.1"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort hosts by ID, name, or creation date. Prefix with a hyphen for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListDatabaseHostsSortQueryParameter"
},
"examples": {
"default": {
"value": "-created_at"
}
}
},
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports node and databases.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListDatabaseHostsIncludeQueryParameter"
},
"examples": {
"default": {
"value": "node"
}
}
}
],
"responses": {
"200": {
"description": "Database hosts returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminDatabaseHostPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Create database host
Creates a database host after validating that the panel can connect to it.
## POST /api/admin/database-hosts
```json
{
"summary": "Create database host",
"operationId": "adminCreateDatabaseHost",
"description": "Creates a database host after validating that the panel can connect to it.",
"parameters": [],
"responses": {
"201": {
"description": "Database host created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminDatabaseHostResource"
}
}
}
},
"400": {
"description": "The panel could not connect to the database host.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminCreateDatabaseHostRequest"
}
}
}
}
}
```
# Get database host
Returns a single database host by ID.
## GET /api/admin/database-hosts/{databaseHost_id}
```json
{
"summary": "Get database host",
"operationId": "adminGetDatabaseHost",
"description": "Returns a single database host by ID.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports node and databases.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminGetDatabaseHostIncludeQueryParameter"
},
"examples": {
"default": {
"value": "node"
}
}
}
],
"responses": {
"200": {
"description": "Database host returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminDatabaseHostResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Delete database host
Deletes a database host when no server databases depend on it.
## DELETE /api/admin/database-hosts/{databaseHost_id}
```json
{
"summary": "Delete database host",
"operationId": "adminDeleteDatabaseHost",
"description": "Deletes a database host when no server databases depend on it.",
"parameters": [],
"responses": {
"204": {
"description": "Database host deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Update database host
Updates a database host after validating that the panel can connect to the supplied host details.
## PUT /api/admin/database-hosts/{databaseHost_id}
```json
{
"summary": "Update database host",
"operationId": "adminUpdateDatabaseHost",
"description": "Updates a database host after validating that the panel can connect to the supplied host details.",
"parameters": [],
"responses": {
"200": {
"description": "Database host updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminDatabaseHostResource"
}
}
}
},
"400": {
"description": "The panel could not connect to the database host.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateDatabaseHostRequest"
}
}
}
}
}
```
# List database host databases
Returns a paginated list of server databases assigned to a database host.
## GET /api/admin/database-hosts/{databaseHost_id}/databases
```json
{
"summary": "List database host databases",
"operationId": "adminListDatabaseHostDatabases",
"description": "Returns a paginated list of server databases assigned to a database host.",
"parameters": [
{
"in": "query",
"name": "page",
"description": "Page number to retrieve.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListDatabaseHostDatabasesPageQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Results to return per page.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListDatabaseHostDatabasesPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
}
],
"responses": {
"200": {
"description": "Databases returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminDatabasePaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# List API keys
Returns a paginated list of application API keys.
## GET /api/admin/api-keys
```json
{
"summary": "List API keys",
"operationId": "adminListAPIKeys",
"description": "Returns a paginated list of application API keys.",
"parameters": [
{
"in": "query",
"name": "filter[memo]",
"description": "Filter keys by memo.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListAPIKeysFilterMemoQueryParameter"
},
"examples": {
"default": {
"value": "Automation"
}
}
},
{
"in": "query",
"name": "filter[identifier]",
"description": "Filter keys by identifier.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListAPIKeysFilterIdentifierQueryParameter"
},
"examples": {
"default": {
"value": "ptla_1234567890abc"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort keys by ID, memo, or creation date. Prefix with a hyphen for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListAPIKeysSortQueryParameter"
},
"examples": {
"default": {
"value": "-created_at"
}
}
},
{
"in": "query",
"name": "page",
"description": "Page number to retrieve.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListAPIKeysPageQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Results to return per page.",
"required": false,
"schema": {
"$ref": "#/components/schemas/AdminListAPIKeysPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
}
],
"responses": {
"200": {
"description": "API keys returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminApiKeyPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Create API key
Creates a new application API key and returns the plaintext secret token once.
## POST /api/admin/api-keys
```json
{
"summary": "Create API key",
"operationId": "adminCreateAPIKey",
"description": "Creates a new application API key and returns the plaintext secret token once.",
"parameters": [],
"responses": {
"201": {
"description": "API key created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminApiKeyResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminCreateAPIKeyRequest"
}
}
}
}
}
```
# Get API key
Returns a single application API key by identifier without exposing its token.
## GET /api/admin/api-keys/{identifier}
```json
{
"summary": "Get API key",
"operationId": "adminGetAPIKey",
"description": "Returns a single application API key by identifier without exposing its token.",
"parameters": [],
"responses": {
"200": {
"description": "API key returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminApiKeyResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Delete API key
Deletes an application API key by identifier.
## DELETE /api/admin/api-keys/{identifier}
```json
{
"summary": "Delete API key",
"operationId": "adminDeleteAPIKey",
"description": "Deletes an application API key by identifier.",
"parameters": [],
"responses": {
"204": {
"description": "API key deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# Update API key
Updates an application API key memo and resource permissions without regenerating the token.
## PUT /api/admin/api-keys/{identifier}
```json
{
"summary": "Update API key",
"operationId": "adminUpdateAPIKey",
"description": "Updates an application API key memo and resource permissions without regenerating the token.",
"parameters": [],
"responses": {
"200": {
"description": "API key updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminApiKeyResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminUpdateAPIKeyRequest"
}
}
}
}
}
```
# Get version information
Returns panel, Wings, and project version metadata.
## GET /api/admin/version
```json
{
"summary": "Get version information",
"operationId": "adminGetVersionInformation",
"description": "Returns panel, Wings, and project version metadata.",
"parameters": [],
"responses": {
"200": {
"description": "Version information returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminVersionInformationResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Admin API"
]
}
```
# List users
Returns a paginated list of panel users visible to the application API key.
## GET /api/application/users
```json
{
"summary": "List users",
"operationId": "applicationListUsers",
"description": "Returns a paginated list of panel users visible to the application API key.",
"parameters": [
{
"in": "query",
"name": "per_page",
"description": "Number of users to return per page, up to 100.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListUsersPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
},
{
"in": "query",
"name": "filter[email]",
"description": "Filter users by email address.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListUsersFilterEmailQueryParameter"
},
"examples": {
"default": {
"value": "user@example.com"
}
}
},
{
"in": "query",
"name": "filter[uuid]",
"description": "Filter users by UUID.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListUsersFilterUuidQueryParameter"
},
"examples": {
"default": {
"value": "0d1f6f4d-9f78-4cf9-9d5a-6f7f4c9f4d6a"
}
}
},
{
"in": "query",
"name": "filter[username]",
"description": "Filter users by username.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListUsersFilterUsernameQueryParameter"
},
"examples": {
"default": {
"value": "example-user"
}
}
},
{
"in": "query",
"name": "filter[external_id]",
"description": "Filter users by external identifier.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListUsersFilterExternalIdQueryParameter"
},
"examples": {
"default": {
"value": "billing-system-42"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort users by id or uuid. Prefix with \"-\" for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListUsersSortQueryParameter"
},
"examples": {
"default": {
"value": "-id"
}
}
},
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"servers\" when the key can read servers.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListUsersIncludeQueryParameter"
},
"examples": {
"default": {
"value": "servers"
}
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationUserPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Create user
Creates a new panel user and returns the created resource.
## POST /api/application/users
```json
{
"summary": "Create user",
"operationId": "applicationCreateUser",
"description": "Creates a new panel user and returns the created resource.",
"parameters": [],
"responses": {
"201": {
"description": "User created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationUserResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationCreateUserRequest"
}
}
}
}
}
```
# Get user
Returns a single panel user by internal numeric ID.
## GET /api/application/users/{user_id}
```json
{
"summary": "Get user",
"operationId": "applicationGetUser",
"description": "Returns a single panel user by internal numeric ID.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"servers\" when the key can read servers.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationGetUserIncludeQueryParameter"
},
"examples": {
"default": {
"value": "servers"
}
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationUserResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Update user
Updates account details for an existing panel user.
## PATCH /api/application/users/{user_id}
```json
{
"summary": "Update user",
"operationId": "applicationUpdateUser",
"description": "Updates account details for an existing panel user.",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationUserResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationUpdateUserRequest"
}
}
}
}
}
```
# Delete user
Deletes a panel user by internal numeric ID.
## DELETE /api/application/users/{user_id}
```json
{
"summary": "Delete user",
"operationId": "applicationDeleteUser",
"description": "Deletes a panel user by internal numeric ID.",
"parameters": [],
"responses": {
"204": {
"description": "User deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Get user by external ID
Returns a single panel user by its external identifier.
## GET /api/application/users/external/{external_id}
```json
{
"summary": "Get user by external ID",
"operationId": "applicationGetUserByExternalID",
"description": "Returns a single panel user by its external identifier.",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationUserResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# List nodes
Returns a paginated list of Wings nodes registered on the panel.
## GET /api/application/nodes
```json
{
"summary": "List nodes",
"operationId": "applicationListNodes",
"description": "Returns a paginated list of Wings nodes registered on the panel.",
"parameters": [
{
"in": "query",
"name": "per_page",
"description": "Number of nodes to return per page, up to 100.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListNodesPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
},
{
"in": "query",
"name": "filter[uuid]",
"description": "Filter nodes by UUID.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListNodesFilterUuidQueryParameter"
},
"examples": {
"default": {
"value": "3d8dd7f9-07a1-4d65-8db0-8c8921f2f4f7"
}
}
},
{
"in": "query",
"name": "filter[name]",
"description": "Filter nodes by name.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListNodesFilterNameQueryParameter"
},
"examples": {
"default": {
"value": "Node 1"
}
}
},
{
"in": "query",
"name": "filter[fqdn]",
"description": "Filter nodes by fully qualified domain name.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListNodesFilterFqdnQueryParameter"
},
"examples": {
"default": {
"value": "node.example.com"
}
}
},
{
"in": "query",
"name": "filter[daemon_token_id]",
"description": "Filter nodes by daemon token identifier.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListNodesFilterDaemonTokenIdQueryParameter"
},
"examples": {
"default": {
"value": "abcdefghijklmnop"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort nodes by id, uuid, memory, or disk. Prefix with \"-\" for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListNodesSortQueryParameter"
},
"examples": {
"default": {
"value": "-id"
}
}
},
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"allocations\", \"location\", and \"servers\" when the key can read those resources.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListNodesIncludeQueryParameter"
},
"examples": {
"default": {
"value": "location,allocations"
}
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationNodePaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Create node
Creates a new Wings node and returns the created resource.
## POST /api/application/nodes
```json
{
"summary": "Create node",
"operationId": "applicationCreateNode",
"description": "Creates a new Wings node and returns the created resource.",
"parameters": [],
"responses": {
"201": {
"description": "Node created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationNodeResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationCreateNodeRequest"
}
}
}
}
}
```
# List deployable nodes
Returns nodes that can satisfy the requested memory, disk, and optional location deployment constraints.
## GET /api/application/nodes/deployable
```json
{
"summary": "List deployable nodes",
"operationId": "applicationListDeployableNodes",
"description": "Returns nodes that can satisfy the requested memory, disk, and optional location deployment constraints.",
"parameters": [
{
"in": "query",
"name": "memory",
"description": "Memory required for the deployment, in megabytes.",
"required": true,
"schema": {
"$ref": "#/components/schemas/ApplicationListDeployableNodesMemoryQueryParameter"
},
"examples": {
"default": {
"value": 1024
}
}
},
{
"in": "query",
"name": "disk",
"description": "Disk required for the deployment, in megabytes.",
"required": true,
"schema": {
"$ref": "#/components/schemas/ApplicationListDeployableNodesDiskQueryParameter"
},
"examples": {
"default": {
"value": 10240
}
}
},
{
"in": "query",
"name": "location_ids",
"description": "Restrict results to these location IDs.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListDeployableNodesLocationIdsQueryParameter"
},
"examples": {
"default": {
"value": [
1,
2
]
}
}
},
{
"in": "query",
"name": "page",
"description": "Page number to return. When omitted, all viable nodes are returned without pagination metadata.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListDeployableNodesPageQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Number of nodes to return per page when page is supplied, up to 100.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListDeployableNodesPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
}
],
"responses": {
"200": {
"description": "Deployable nodes returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationNodeListResponse"
}
}
}
},
"400": {
"description": "No nodes can satisfy the requested deployment constraints.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Get node
Returns a single Wings node by internal numeric ID.
## GET /api/application/nodes/{node_id}
```json
{
"summary": "Get node",
"operationId": "applicationGetNode",
"description": "Returns a single Wings node by internal numeric ID.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"allocations\", \"location\", and \"servers\" when the key can read those resources.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationGetNodeIncludeQueryParameter"
},
"examples": {
"default": {
"value": "location,allocations"
}
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationNodeResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Delete node
Deletes a Wings node by internal numeric ID when no servers are attached.
## DELETE /api/application/nodes/{node_id}
```json
{
"summary": "Delete node",
"operationId": "applicationDeleteNode",
"description": "Deletes a Wings node by internal numeric ID when no servers are attached.",
"parameters": [],
"responses": {
"204": {
"description": "Node deleted."
},
"400": {
"description": "The node has active servers attached.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Get node configuration
Returns the Wings configuration payload for a node, including node authentication tokens.
## GET /api/application/nodes/{node_id}/configuration
```json
{
"summary": "Get node configuration",
"operationId": "applicationGetNodeConfiguration",
"description": "Returns the Wings configuration payload for a node, including node authentication tokens.",
"parameters": [],
"responses": {
"200": {
"description": "Node configuration.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationGetNodeConfigurationResponseBody"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Update node
Updates an existing Wings node by internal numeric ID.
## PATCH /api/application/nodes/{id}
```json
{
"summary": "Update node",
"operationId": "applicationUpdateNode",
"description": "Updates an existing Wings node by internal numeric ID.",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationNodeResource"
}
}
}
},
"400": {
"description": "The panel saved the node changes but could not persist the configuration to Wings.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationUpdateNodeRequest"
}
}
}
}
}
```
# List node allocations
Returns a paginated list of allocations belonging to a node.
## GET /api/application/nodes/{node_id}/allocations
```json
{
"summary": "List node allocations",
"operationId": "applicationListNodeAllocations",
"description": "Returns a paginated list of allocations belonging to a node.",
"parameters": [
{
"in": "query",
"name": "per_page",
"description": "Number of allocations to return per page, up to 100.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListNodeAllocationsPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
},
{
"in": "query",
"name": "filter[ip]",
"description": "Filter allocations by exact IP address.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListNodeAllocationsFilterIpQueryParameter"
},
"examples": {
"default": {
"value": "192.0.2.10"
}
}
},
{
"in": "query",
"name": "filter[port]",
"description": "Filter allocations by exact port.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListNodeAllocationsFilterPortQueryParameter"
},
"examples": {
"default": {
"value": 25565
}
}
},
{
"in": "query",
"name": "filter[ip_alias]",
"description": "Filter allocations by IP alias.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListNodeAllocationsFilterIpAliasQueryParameter"
},
"examples": {
"default": {
"value": "minecraft.example.com"
}
}
},
{
"in": "query",
"name": "filter[server_id]",
"description": "Filter allocations by assigned server ID. Empty or non-numeric values return unassigned allocations.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListNodeAllocationsFilterServerIdQueryParameter"
},
"examples": {
"default": {
"value": "1"
}
}
},
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"node\" and \"server\" when the key can read those resources.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListNodeAllocationsIncludeQueryParameter"
},
"examples": {
"default": {
"value": "server"
}
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationAllocationPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Create node allocations
Assigns one or more allocation ports to a node.
## POST /api/application/nodes/{node_id}/allocations
```json
{
"summary": "Create node allocations",
"operationId": "applicationCreateNodeAllocations",
"description": "Assigns one or more allocation ports to a node.",
"parameters": [],
"responses": {
"204": {
"description": "Allocations created."
},
"400": {
"description": "The allocation IP, CIDR, or port mapping could not be processed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationCreateNodeAllocationsRequest"
}
}
}
}
}
```
# Delete node allocation
Deletes an allocation from a node when no server is using it.
## DELETE /api/application/nodes/{node_id}/allocations/{allocation_id}
```json
{
"summary": "Delete node allocation",
"operationId": "applicationDeleteNodeAllocation",
"description": "Deletes an allocation from a node when no server is using it.",
"parameters": [],
"responses": {
"204": {
"description": "Allocation deleted."
},
"400": {
"description": "The allocation is currently assigned to a server.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# List locations
Returns a paginated list of all locations registered on the panel.
## GET /api/application/locations
```json
{
"summary": "List locations",
"operationId": "applicationListLocations",
"description": "Returns a paginated list of all locations registered on the panel.",
"parameters": [
{
"in": "query",
"name": "per_page",
"description": "Number of locations to return per page, up to 100.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListLocationsPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
},
{
"in": "query",
"name": "filter[short]",
"description": "Filter locations by short identifier.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListLocationsFilterShortQueryParameter"
},
"examples": {
"default": {
"value": "us-east"
}
}
},
{
"in": "query",
"name": "filter[long]",
"description": "Filter locations by description.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListLocationsFilterLongQueryParameter"
},
"examples": {
"default": {
"value": "US East Datacenter"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort locations by id. Prefix with \"-\" for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListLocationsSortQueryParameter"
},
"examples": {
"default": {
"value": "id"
}
}
},
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"nodes\" and \"servers\" when the key can read those resources.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListLocationsIncludeQueryParameter"
},
"examples": {
"default": {
"value": "nodes,servers"
}
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationLocationPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Create location
Creates a new location and returns the created resource.
## POST /api/application/locations
```json
{
"summary": "Create location",
"operationId": "applicationCreateLocation",
"description": "Creates a new location and returns the created resource.",
"parameters": [],
"responses": {
"201": {
"description": "Location created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationLocationResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationCreateLocationRequest"
}
}
}
}
}
```
# Get location
Returns a single location by internal numeric ID.
## GET /api/application/locations/{location_id}
```json
{
"summary": "Get location",
"operationId": "applicationGetLocation",
"description": "Returns a single location by internal numeric ID.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"nodes\" and \"servers\" when the key can read those resources.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationGetLocationIncludeQueryParameter"
},
"examples": {
"default": {
"value": "nodes,servers"
}
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationLocationResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Update location
Updates an existing location by internal numeric ID.
## PATCH /api/application/locations/{location_id}
```json
{
"summary": "Update location",
"operationId": "applicationUpdateLocation",
"description": "Updates an existing location by internal numeric ID.",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationLocationResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationUpdateLocationRequest"
}
}
}
}
}
```
# Delete location
Deletes a location by internal numeric ID.
## DELETE /api/application/locations/{location_id}
```json
{
"summary": "Delete location",
"operationId": "applicationDeleteLocation",
"description": "Deletes a location by internal numeric ID.",
"parameters": [],
"responses": {
"204": {
"description": "Location deleted."
},
"400": {
"description": "The location has active nodes attached.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# List eggs
Returns a paginated list of server eggs available on the panel.
## GET /api/application/eggs
```json
{
"summary": "List eggs",
"operationId": "applicationListEggs",
"description": "Returns a paginated list of server eggs available on the panel.",
"parameters": [
{
"in": "query",
"name": "per_page",
"description": "Number of eggs to return per page, up to 100.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListEggsPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
},
{
"in": "query",
"name": "filter[name]",
"description": "Filter eggs by name.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListEggsFilterNameQueryParameter"
},
"examples": {
"default": {
"value": "Paper"
}
}
},
{
"in": "query",
"name": "filter[tag]",
"description": "Filter eggs by tag slug. Separate several slugs with commas to return eggs that carry any of them.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListEggsFilterTagQueryParameter"
},
"examples": {
"default": {
"value": "minecraft"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort eggs by id or name. Prefix with \"-\" for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListEggsSortQueryParameter"
},
"examples": {
"default": {
"value": "name"
}
}
},
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"servers\", \"config\", \"script\", \"variables\", and \"tags\" when the key can read those resources.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListEggsIncludeQueryParameter"
},
"examples": {
"default": {
"value": "variables"
}
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationEggPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Get egg
Returns a single egg by internal numeric ID.
## GET /api/application/eggs/{egg_id}
```json
{
"summary": "Get egg",
"operationId": "applicationGetEgg",
"description": "Returns a single egg by internal numeric ID.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"servers\", \"config\", \"script\", \"variables\", and \"tags\" when the key can read those resources.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationGetEggIncludeQueryParameter"
},
"examples": {
"default": {
"value": "variables"
}
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationEggResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# List tags
Returns a paginated list of the tags that group eggs.
## GET /api/application/tags
```json
{
"summary": "List tags",
"operationId": "applicationListTags",
"description": "Returns a paginated list of the tags that group eggs.",
"parameters": [
{
"in": "query",
"name": "per_page",
"description": "Number of tags to return per page, up to 100.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListTagsPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
},
{
"in": "query",
"name": "filter[name]",
"description": "Filter tags by name.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListTagsFilterNameQueryParameter"
},
"examples": {
"default": {
"value": "Minecraft"
}
}
},
{
"in": "query",
"name": "filter[slug]",
"description": "Filter tags by slug.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListTagsFilterSlugQueryParameter"
},
"examples": {
"default": {
"value": "minecraft"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort tags by id, name, or slug. Prefix with \"-\" for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListTagsSortQueryParameter"
},
"examples": {
"default": {
"value": "name"
}
}
},
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"eggs\" and \"nodes\" when the key can read those resources.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListTagsIncludeQueryParameter"
},
"examples": {
"default": {
"value": "eggs"
}
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationTagPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Get tag
Returns a single tag by internal numeric ID.
## GET /api/application/tags/{tag_id}
```json
{
"summary": "Get tag",
"operationId": "applicationGetTag",
"description": "Returns a single tag by internal numeric ID.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"eggs\" and \"nodes\" when the key can read those resources.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationGetTagIncludeQueryParameter"
},
"examples": {
"default": {
"value": "eggs"
}
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationTagResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# List servers
Returns a paginated list of servers visible to the application API key.
## GET /api/application/servers
```json
{
"summary": "List servers",
"operationId": "applicationListServers",
"description": "Returns a paginated list of servers visible to the application API key.",
"parameters": [
{
"in": "query",
"name": "per_page",
"description": "Number of servers to return per page, up to 100.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListServersPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
},
{
"in": "query",
"name": "filter[uuid]",
"description": "Filter servers by UUID.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListServersFilterUuidQueryParameter"
},
"examples": {
"default": {
"value": "4fcb1f44-0f90-4a1a-a8bf-7cc8a14d0f26"
}
}
},
{
"in": "query",
"name": "filter[uuidShort]",
"description": "Filter servers by short UUID identifier.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListServersFilterUuidShortQueryParameter"
},
"examples": {
"default": {
"value": "4fcb1f44"
}
}
},
{
"in": "query",
"name": "filter[name]",
"description": "Filter servers by name.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListServersFilterNameQueryParameter"
},
"examples": {
"default": {
"value": "Minecraft Server"
}
}
},
{
"in": "query",
"name": "filter[description]",
"description": "Filter servers by description.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListServersFilterDescriptionQueryParameter"
},
"examples": {
"default": {
"value": "Production"
}
}
},
{
"in": "query",
"name": "filter[image]",
"description": "Filter servers by Docker image.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListServersFilterImageQueryParameter"
},
"examples": {
"default": {
"value": "ghcr.io/pterodactyl/yolks:java_23"
}
}
},
{
"in": "query",
"name": "filter[external_id]",
"description": "Filter servers by external identifier.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListServersFilterExternalIdQueryParameter"
},
"examples": {
"default": {
"value": "billing-server-42"
}
}
},
{
"in": "query",
"name": "search",
"description": "Search term accepted by the list request. Must not exceed 100 characters.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListServersSearchQueryParameter"
},
"examples": {
"default": {
"value": "minecraft"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort servers by id or uuid. Prefix with \"-\" for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListServersSortQueryParameter"
},
"examples": {
"default": {
"value": "-id"
}
}
},
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"allocations\", \"user\", \"subusers\", \"egg\", \"variables\", \"location\", \"node\", \"databases\", and \"transfer\" when the key can read those resources.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListServersIncludeQueryParameter"
},
"examples": {
"default": {
"value": "user,node,allocations"
}
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationServerPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Create server
Creates a new server using either explicit allocations or automatic deployment constraints.
## POST /api/application/servers
```json
{
"summary": "Create server",
"operationId": "applicationCreateServer",
"description": "Creates a new server using either explicit allocations or automatic deployment constraints.",
"parameters": [],
"responses": {
"201": {
"description": "Server created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationServerResource"
}
}
}
},
"400": {
"description": "Automatic deployment could not find a viable node or allocation.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationCreateServerRequest"
}
}
}
}
}
```
# Get server
Returns a single server by internal numeric ID.
## GET /api/application/servers/{server_id}
```json
{
"summary": "Get server",
"operationId": "applicationGetServer",
"description": "Returns a single server by internal numeric ID.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"allocations\", \"user\", \"subusers\", \"egg\", \"variables\", \"location\", \"node\", \"databases\", and \"transfer\" when the key can read those resources.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationGetServerIncludeQueryParameter"
},
"examples": {
"default": {
"value": "user,node,allocations"
}
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationServerResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Delete server
Deletes a server. The force route bypasses recoverable Wings or database host deletion failures.
## DELETE /api/application/servers/{server_id}
```json
{
"summary": "Delete server",
"operationId": "applicationDeleteServerForServersServerId",
"description": "Deletes a server. The force route bypasses recoverable Wings or database host deletion failures.",
"parameters": [],
"responses": {
"204": {
"description": "Server deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Get server by external ID
Returns a single server by its external identifier.
## GET /api/application/servers/external/{external_id}
```json
{
"summary": "Get server by external ID",
"operationId": "applicationGetServerByExternalID",
"description": "Returns a single server by its external identifier.",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationServerResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Update server details
Updates ownership and display details for a server.
## PATCH /api/application/servers/{server_id}/details
```json
{
"summary": "Update server details",
"operationId": "applicationUpdateServerDetails",
"description": "Updates ownership and display details for a server.",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationServerResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationUpdateServerDetailsRequest"
}
}
}
}
}
```
# Update server build
Updates resource limits, feature limits, and allocations for a server.
## PATCH /api/application/servers/{server_id}/build
```json
{
"summary": "Update server build",
"operationId": "applicationUpdateServerBuild",
"description": "Updates resource limits, feature limits, and allocations for a server.",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationServerResource"
}
}
}
},
"400": {
"description": "The requested allocation change is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationUpdateServerBuildRequest"
}
}
}
}
}
```
# Update server startup
Updates startup command, image, egg, and environment variables for a server.
## PATCH /api/application/servers/{server_id}/startup
```json
{
"summary": "Update server startup",
"operationId": "applicationUpdateServerStartup",
"description": "Updates startup command, image, egg, and environment variables for a server.",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationServerResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
},
"504": {
"description": "Wings could not be reached while syncing startup changes.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationUpdateServerStartupRequest"
}
}
}
}
}
```
# Suspend server
Marks a server as suspended and syncs the state to Wings.
## POST /api/application/servers/{server_id}/suspend
```json
{
"summary": "Suspend server",
"operationId": "applicationSuspendServer",
"description": "Marks a server as suspended and syncs the state to Wings.",
"parameters": [],
"responses": {
"204": {
"description": "Server suspended."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "The server is currently being transferred.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"504": {
"description": "Wings could not be reached while syncing suspension state.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Unsuspend server
Clears a server suspension and syncs the state to Wings.
## POST /api/application/servers/{server_id}/unsuspend
```json
{
"summary": "Unsuspend server",
"operationId": "applicationUnsuspendServer",
"description": "Clears a server suspension and syncs the state to Wings.",
"parameters": [],
"responses": {
"204": {
"description": "Server unsuspended."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"409": {
"description": "The server is currently being transferred.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"504": {
"description": "Wings could not be reached while syncing suspension state.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Reinstall server
Marks a server for reinstall and asks Wings to reinstall it.
## POST /api/application/servers/{server_id}/reinstall
```json
{
"summary": "Reinstall server",
"operationId": "applicationReinstallServer",
"description": "Marks a server for reinstall and asks Wings to reinstall it.",
"parameters": [],
"responses": {
"204": {
"description": "Server marked for reinstall."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"504": {
"description": "Wings could not be reached while starting reinstall.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Delete server
Deletes a server. The force route bypasses recoverable Wings or database host deletion failures.
## DELETE /api/application/servers/{server_id}/{force}
```json
{
"summary": "Delete server",
"operationId": "applicationDeleteServerForServersServerIdForce",
"description": "Deletes a server. The force route bypasses recoverable Wings or database host deletion failures.",
"parameters": [],
"responses": {
"204": {
"description": "Server deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# List server databases
Returns all databases attached to a server.
## GET /api/application/servers/{server_id}/databases
```json
{
"summary": "List server databases",
"operationId": "applicationListServerDatabases",
"description": "Returns all databases attached to a server.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"password\" and \"host\" when the key can read database hosts.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationListServerDatabasesIncludeQueryParameter"
},
"examples": {
"default": {
"value": "password,host"
}
}
}
],
"responses": {
"200": {
"description": "Server databases returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationServerDatabaseListResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Create server database
Creates a database for a server on a configured database host.
## POST /api/application/servers/{server_id}/databases
```json
{
"summary": "Create server database",
"operationId": "applicationCreateServerDatabase",
"description": "Creates a database for a server on a configured database host.",
"parameters": [],
"responses": {
"201": {
"description": "Database created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationServerDatabaseResource"
}
}
}
},
"400": {
"description": "The server has reached its configured database limit.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationCreateServerDatabaseRequest"
}
}
}
}
}
```
# Get server database
Returns a single database attached to a server.
## GET /api/application/servers/{server_id}/databases/{database_id}
```json
{
"summary": "Get server database",
"operationId": "applicationGetServerDatabase",
"description": "Returns a single database attached to a server.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"password\" and \"host\" when the key can read database hosts.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ApplicationGetServerDatabaseIncludeQueryParameter"
},
"examples": {
"default": {
"value": "password,host"
}
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApplicationServerDatabaseResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Delete server database
Deletes a database attached to a server.
## DELETE /api/application/servers/{server_id}/databases/{database_id}
```json
{
"summary": "Delete server database",
"operationId": "applicationDeleteServerDatabase",
"description": "Deletes a database attached to a server.",
"parameters": [],
"responses": {
"204": {
"description": "Database deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Reset server database password
Rotates the password for a database attached to a server.
## POST /api/application/servers/{server_id}/databases/{database_id}/reset-password
```json
{
"summary": "Reset server database password",
"operationId": "applicationResetServerDatabasePassword",
"description": "Rotates the password for a database attached to a server.",
"parameters": [],
"responses": {
"204": {
"description": "Database password reset."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Application API"
]
}
```
# Login
Authenticates a browser session. Returns a complete login payload, or a two-factor checkpoint token when the account requires a second factor.
## POST /auth/login
```json
{
"summary": "Login",
"operationId": "authLogin",
"description": "Authenticates a browser session. Returns a complete login payload, or a two-factor checkpoint token when the account requires a second factor.",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthLoginResponse"
}
}
}
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Authentication"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthLoginRequest"
}
}
}
},
"security": []
}
```
# Complete login checkpoint
Completes a two-factor login checkpoint using either a TOTP code or a recovery token.
## POST /auth/login/checkpoint
```json
{
"summary": "Complete login checkpoint",
"operationId": "authCompleteLoginCheckpoint",
"description": "Completes a two-factor login checkpoint using either a TOTP code or a recovery token.",
"parameters": [],
"responses": {
"200": {
"description": "Login completed and a browser session was created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthLoginCompleteResponse"
}
}
}
},
"400": {
"description": "The checkpoint token, TOTP code, or recovery token is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Authentication"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthCompleteLoginCheckpointRequest"
}
}
}
},
"security": []
}
```
# Request password reset email
Sends a password reset email when the account exists. The response is intentionally the same when no matching account is found.
## POST /auth/password
```json
{
"summary": "Request password reset email",
"operationId": "authRequestPasswordResetEmail",
"description": "Sends a password reset email when the account exists. The response is intentionally the same when no matching account is found.",
"parameters": [],
"responses": {
"200": {
"description": "Password reset email accepted.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthPasswordResetEmailResponse"
}
}
}
},
"400": {
"description": "The reCAPTCHA token is invalid or missing.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Authentication"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthRequestPasswordResetEmailRequest"
}
}
}
},
"security": []
}
```
# Reset password
Resets an account password using a password reset token. The response tells the frontend whether the user must return to the login form.
## POST /auth/password/reset
```json
{
"summary": "Reset password",
"operationId": "authResetPassword",
"description": "Resets an account password using a password reset token. The response tells the frontend whether the user must return to the login form.",
"parameters": [],
"responses": {
"200": {
"description": "Password reset completed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthResetPasswordResponse"
}
}
}
},
"400": {
"description": "The reset token is invalid or expired.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Authentication"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthResetPasswordRequest"
}
}
}
},
"security": []
}
```
# Logout
Destroys the current browser session and regenerates the CSRF token.
## POST /auth/logout
```json
{
"summary": "Logout",
"operationId": "authLogout",
"description": "Destroys the current browser session and regenerates the CSRF token.",
"parameters": [],
"responses": {
"204": {
"description": "The browser session was destroyed."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Authentication"
],
"security": [
{
"panelSession": []
}
]
}
```
# Get CSRF cookie
Initializes the Laravel Sanctum CSRF cookie required before browser-session authentication requests.
## GET /sanctum/csrf-cookie
```json
{
"summary": "Get CSRF cookie",
"operationId": "authGetCsrfCookie",
"description": "Initializes the Laravel Sanctum CSRF cookie required before browser-session authentication requests.",
"parameters": [],
"responses": {
"204": {
"description": "The CSRF cookie was queued on the response."
}
},
"tags": [
"Authentication"
],
"security": []
}
```
# List client servers
Returns a paginated list of servers the authenticated user can access as an owner, subuser, or administrator.
## GET /api/client
```json
{
"summary": "List client servers",
"operationId": "clientListClientServers",
"description": "Returns a paginated list of servers the authenticated user can access as an owner, subuser, or administrator.",
"parameters": [
{
"in": "query",
"name": "page",
"description": "Page number to return.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListClientServersPageQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Number of servers to return per page. The maximum is 100.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListClientServersPerPageQueryParameter"
},
"examples": {
"default": {
"value": 50
}
}
},
{
"in": "query",
"name": "type",
"description": "Scope of servers to return. \"owner\" returns owned servers, \"admin\" returns admin-accessible servers excluding owned or subuser servers, and \"admin-all\" returns all servers for root administrators.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListClientServersTypeQueryParameter"
},
"examples": {
"default": {
"value": "owner"
}
}
},
{
"in": "query",
"name": "filter[uuid]",
"description": "Filter servers by UUID.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListClientServersFilterUuidQueryParameter"
},
"examples": {
"default": {
"value": "4fcb1f44-0f90-4a1a-a8bf-7cc8a14d0f26"
}
}
},
{
"in": "query",
"name": "filter[name]",
"description": "Filter servers by name.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListClientServersFilterNameQueryParameter"
},
"examples": {
"default": {
"value": "Minecraft Server"
}
}
},
{
"in": "query",
"name": "filter[description]",
"description": "Filter servers by description.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListClientServersFilterDescriptionQueryParameter"
},
"examples": {
"default": {
"value": "Production"
}
}
},
{
"in": "query",
"name": "filter[external_id]",
"description": "Filter servers by external identifier.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListClientServersFilterExternalIdQueryParameter"
},
"examples": {
"default": {
"value": "billing-server-42"
}
}
},
{
"in": "query",
"name": "filter[*]",
"description": "Search across server name, UUID, short UUID, external identifier, or allocation address and port.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListClientServersFilterWildcardQueryParameter"
},
"examples": {
"default": {
"value": "192.0.2.10:25565"
}
}
}
],
"responses": {
"200": {
"description": "Accessible servers returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientServerPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# List server permissions
Returns the permission keys that can be granted to server subusers.
## GET /api/client/permissions
```json
{
"summary": "List server permissions",
"operationId": "clientListServerPermissions",
"description": "Returns the permission keys that can be granted to server subusers.",
"parameters": [],
"responses": {
"200": {
"description": "System permission definitions returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientSystemPermissionsResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Get account
Returns profile details for the authenticated user.
## GET /api/client/account
```json
{
"summary": "Get account",
"operationId": "clientGetAccount",
"description": "Returns profile details for the authenticated user.",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientUserResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Get two-factor setup
Creates or returns the current two-factor setup payload for the authenticated user.
## GET /api/client/account/two-factor
```json
{
"summary": "Get two-factor setup",
"operationId": "clientGetTwoFactorSetup",
"description": "Creates or returns the current two-factor setup payload for the authenticated user.",
"parameters": [],
"responses": {
"200": {
"description": "Two-factor setup data returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientTwoFactorSetupResponse"
}
}
}
},
"400": {
"description": "Two-factor authentication is already enabled.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Enable two-factor authentication
Enables two-factor authentication for the authenticated user and returns one-time recovery tokens.
## POST /api/client/account/two-factor
```json
{
"summary": "Enable two-factor authentication",
"operationId": "clientEnableTwoFactorAuthentication",
"description": "Enables two-factor authentication for the authenticated user and returns one-time recovery tokens.",
"parameters": [],
"responses": {
"200": {
"description": "Two-factor authentication enabled. Store these recovery tokens now; they are not shown again.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientRecoveryTokensResource"
}
}
}
},
"400": {
"description": "The password or TOTP code is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientEnableTwoFactorAuthenticationRequest"
}
}
}
}
}
```
# Disable two-factor authentication
Disables two-factor authentication for the authenticated user after verifying their password.
## POST /api/client/account/two-factor/disable
```json
{
"summary": "Disable two-factor authentication",
"operationId": "clientDisableTwoFactorAuthentication",
"description": "Disables two-factor authentication for the authenticated user after verifying their password.",
"parameters": [],
"responses": {
"204": {
"description": "Two-factor authentication disabled."
},
"400": {
"description": "The password is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientDisableTwoFactorAuthenticationRequest"
}
}
}
}
}
```
# Update account email
Updates the authenticated user's email address after verifying their current password.
## PUT /api/client/account/email
```json
{
"summary": "Update account email",
"operationId": "clientUpdateAccountEmail",
"description": "Updates the authenticated user's email address after verifying their current password.",
"parameters": [],
"responses": {
"204": {
"description": "Email address updated."
},
"400": {
"description": "The current password is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
},
"429": {
"description": "The account has changed email too many times in the current throttle window.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientUpdateAccountEmailRequest"
}
}
}
}
}
```
# Update account password
Updates the authenticated user's password and revokes other active sessions where supported.
## PUT /api/client/account/password
```json
{
"summary": "Update account password",
"operationId": "clientUpdateAccountPassword",
"description": "Updates the authenticated user's password and revokes other active sessions where supported.",
"parameters": [],
"responses": {
"204": {
"description": "Password updated."
},
"400": {
"description": "The current password is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientUpdateAccountPasswordRequest"
}
}
}
}
}
```
# List account activity
Returns a paginated list of activity log entries for the authenticated user account.
## GET /api/client/account/activity
```json
{
"summary": "List account activity",
"operationId": "clientListAccountActivity",
"description": "Returns a paginated list of activity log entries for the authenticated user account.",
"parameters": [
{
"in": "query",
"name": "page",
"description": "Page number to retrieve.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListAccountActivityPageQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Number of activity log entries to return per page. The maximum is 100.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListAccountActivityPerPageQueryParameter"
},
"examples": {
"default": {
"value": 25
}
}
},
{
"in": "query",
"name": "filter[event]",
"description": "Filter activity by event name.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListAccountActivityFilterEventQueryParameter"
},
"examples": {
"default": {
"value": "user:account.email-changed"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort activity by timestamp. Prefix with \"-\" for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListAccountActivitySortQueryParameter"
},
"examples": {
"default": {
"value": "-timestamp"
}
}
},
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports actor.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListAccountActivityIncludeQueryParameter"
},
"examples": {
"default": {
"value": "actor"
}
}
}
],
"responses": {
"200": {
"description": "Activity logs returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientActivityLogPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# List account API keys
Returns all client API keys owned by the authenticated user.
## GET /api/client/account/api-keys
```json
{
"summary": "List account API keys",
"operationId": "clientListAccountAPIKeys",
"description": "Returns all client API keys owned by the authenticated user.",
"parameters": [],
"responses": {
"200": {
"description": "API keys returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientApiKeyListResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Create account API key
Creates a client API key for the authenticated user and returns the secret token once.
## POST /api/client/account/api-keys
```json
{
"summary": "Create account API key",
"operationId": "clientCreateAccountAPIKey",
"description": "Creates a client API key for the authenticated user and returns the secret token once.",
"parameters": [],
"responses": {
"200": {
"description": "API key created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientApiKeyResource"
}
}
}
},
"400": {
"description": "The account already has the maximum number of API keys.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientCreateAccountAPIKeyRequest"
}
}
}
}
}
```
# Delete account API key
Deletes one client API key owned by the authenticated user.
## DELETE /api/client/account/api-keys/{identifier}
```json
{
"summary": "Delete account API key",
"operationId": "clientDeleteAccountAPIKey",
"description": "Deletes one client API key owned by the authenticated user.",
"parameters": [],
"responses": {
"204": {
"description": "API key deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# List SSH keys
Returns all SSH public keys configured for the authenticated user.
## GET /api/client/account/ssh-keys
```json
{
"summary": "List SSH keys",
"operationId": "clientListSSHKeys",
"description": "Returns all SSH public keys configured for the authenticated user.",
"parameters": [],
"responses": {
"200": {
"description": "SSH keys returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientSshKeyListResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Create SSH key
Adds an SSH public key to the authenticated user account.
## POST /api/client/account/ssh-keys
```json
{
"summary": "Create SSH key",
"operationId": "clientCreateSSHKey",
"description": "Adds an SSH public key to the authenticated user account.",
"parameters": [],
"responses": {
"200": {
"description": "SSH key created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientSshKeyResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientCreateSSHKeyRequest"
}
}
}
}
}
```
# Delete SSH key
Deletes an SSH key from the authenticated user account by fingerprint. Missing keys are treated as already deleted.
## POST /api/client/account/ssh-keys/remove
```json
{
"summary": "Delete SSH key",
"operationId": "clientDeleteSSHKey",
"description": "Deletes an SSH key from the authenticated user account by fingerprint. Missing keys are treated as already deleted.",
"parameters": [],
"responses": {
"204": {
"description": "SSH key deleted or already absent."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientDeleteSSHKeyRequest"
}
}
}
}
}
```
# Get server
Returns details for one server the authenticated user can access.
## GET /api/client/servers/{server_uuid}
```json
{
"summary": "Get server",
"operationId": "clientGetServer",
"description": "Returns details for one server the authenticated user can access.",
"parameters": [],
"responses": {
"200": {
"description": "Server details returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientServerResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Get websocket credentials
Returns a short-lived Wings websocket token and socket URL for the server.
## GET /api/client/servers/{server_uuid}/websocket
```json
{
"summary": "Get websocket credentials",
"operationId": "clientGetWebsocketCredentials",
"description": "Returns a short-lived Wings websocket token and socket URL for the server.",
"parameters": [],
"responses": {
"200": {
"description": "Websocket credentials returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientGetWebsocketCredentialsResponseBody"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Get server resources
Returns the latest cached resource utilization reported by Wings for the server.
## GET /api/client/servers/{server_uuid}/resources
```json
{
"summary": "Get server resources",
"operationId": "clientGetServerResources",
"description": "Returns the latest cached resource utilization reported by Wings for the server.",
"parameters": [],
"responses": {
"200": {
"description": "Resource utilization returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientStatsResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"502": {
"description": "Wings could not be reached for resource utilization.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# List server activity
Returns a paginated list of activity log entries for the server.
## GET /api/client/servers/{server_uuid}/activity
```json
{
"summary": "List server activity",
"operationId": "clientListServerActivity",
"description": "Returns a paginated list of activity log entries for the server.",
"parameters": [
{
"in": "query",
"name": "page",
"description": "Page number to retrieve.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListServerActivityPageQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Number of activity log entries to return per page. The maximum is 100.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListServerActivityPerPageQueryParameter"
},
"examples": {
"default": {
"value": 25
}
}
},
{
"in": "query",
"name": "filter[event]",
"description": "Filter activity by event name.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListServerActivityFilterEventQueryParameter"
},
"examples": {
"default": {
"value": "server:power.start"
}
}
},
{
"in": "query",
"name": "sort",
"description": "Sort activity by timestamp. Prefix with \"-\" for descending order.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListServerActivitySortQueryParameter"
},
"examples": {
"default": {
"value": "-timestamp"
}
}
},
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports actor.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListServerActivityIncludeQueryParameter"
},
"examples": {
"default": {
"value": "actor"
}
}
}
],
"responses": {
"200": {
"description": "Server activity logs returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientActivityLogPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Send server command
Sends a console command to the running server through Wings.
## POST /api/client/servers/{server_uuid}/command
```json
{
"summary": "Send server command",
"operationId": "clientSendServerCommand",
"description": "Sends a console command to the running server through Wings.",
"parameters": [],
"responses": {
"204": {
"description": "Command accepted by Wings."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
},
"502": {
"description": "The server is not online or Wings rejected the command.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientSendServerCommandRequest"
}
}
}
}
}
```
# Send power action
Sends a power signal to the server through Wings.
## POST /api/client/servers/{server_uuid}/power
```json
{
"summary": "Send power action",
"operationId": "clientSendPowerAction",
"description": "Sends a power signal to the server through Wings.",
"parameters": [],
"responses": {
"204": {
"description": "Power signal accepted by Wings."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientSendPowerActionRequest"
}
}
}
}
}
```
# List server databases
Returns databases attached to a server visible to the authenticated user.
## GET /api/client/servers/{server_uuid}/databases
```json
{
"summary": "List server databases",
"operationId": "clientListServerDatabases",
"description": "Returns databases attached to a server visible to the authenticated user.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"password\" when the user can view database passwords.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListServerDatabasesIncludeQueryParameter"
},
"examples": {
"default": {
"value": "password"
}
}
}
],
"responses": {
"200": {
"description": "Server databases returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientServerDatabaseListResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Create server database
Creates a database for a server on an automatically selected database host.
## POST /api/client/servers/{server_uuid}/databases
```json
{
"summary": "Create server database",
"operationId": "clientCreateServerDatabase",
"description": "Creates a database for a server on an automatically selected database host.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"password\" when the user can view database passwords.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientCreateServerDatabaseIncludeQueryParameter"
},
"examples": {
"default": {
"value": "password"
}
}
}
],
"responses": {
"200": {
"description": "Database created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientServerDatabaseResource"
}
}
}
},
"400": {
"description": "The server has reached its configured database limit or client database creation is disabled.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientCreateServerDatabaseRequest"
}
}
}
}
}
```
# Rotate server database password
Rotates the password for a database attached to a server and returns the updated database.
## POST /api/client/servers/{server_uuid}/databases/{database_id}/rotate-password
```json
{
"summary": "Rotate server database password",
"operationId": "clientRotateServerDatabasePassword",
"description": "Rotates the password for a database attached to a server and returns the updated database.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports \"password\" when the user can view database passwords.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientRotateServerDatabasePasswordIncludeQueryParameter"
},
"examples": {
"default": {
"value": "password"
}
}
}
],
"responses": {
"200": {
"description": "Database password rotated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientServerDatabaseResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Delete server database
Deletes a database attached to a server.
## DELETE /api/client/servers/{server_uuid}/databases/{database_id}
```json
{
"summary": "Delete server database",
"operationId": "clientDeleteServerDatabase",
"description": "Deletes a database attached to a server.",
"parameters": [],
"responses": {
"204": {
"description": "Database deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# List files
Returns files and folders in a server directory.
## GET /api/client/servers/{server_uuid}/files/list
```json
{
"summary": "List files",
"operationId": "clientListFiles",
"description": "Returns files and folders in a server directory.",
"parameters": [
{
"in": "query",
"name": "directory",
"description": "Directory to list. Defaults to the server root.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListFilesDirectoryQueryParameter"
},
"examples": {
"default": {
"value": "/config"
}
}
}
],
"responses": {
"200": {
"description": "Directory contents returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientFileObjectListResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
},
"502": {
"description": "Wings could not list the directory.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Get file contents
Returns raw text contents for a server file.
## GET /api/client/servers/{server_uuid}/files/contents
```json
{
"summary": "Get file contents",
"operationId": "clientGetFileContents",
"description": "Returns raw text contents for a server file.",
"parameters": [
{
"in": "query",
"name": "file",
"description": "Path to the file to read.",
"required": true,
"schema": {
"$ref": "#/components/schemas/ClientGetFileContentsFileQueryParameter"
},
"examples": {
"default": {
"value": "/server.properties"
}
}
}
],
"responses": {
"200": {
"description": "File contents returned as text/plain.",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ClientGetFileContentsResponseBody"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
},
"502": {
"description": "Wings could not read the file.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Get file download URL
Returns a short-lived signed URL for downloading a server file directly from Wings.
## GET /api/client/servers/{server_uuid}/files/download
```json
{
"summary": "Get file download URL",
"operationId": "clientGetFileDownloadURL",
"description": "Returns a short-lived signed URL for downloading a server file directly from Wings.",
"parameters": [
{
"in": "query",
"name": "file",
"description": "Path to the file to download.",
"required": true,
"schema": {
"$ref": "#/components/schemas/ClientGetFileDownloadURLFileQueryParameter"
},
"examples": {
"default": {
"value": "/server.properties"
}
}
}
],
"responses": {
"200": {
"description": "Signed download URL returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientSignedUrlResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
},
"502": {
"description": "Wings could not create the download URL.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Rename files
Renames one or more files or folders in a server directory.
## PUT /api/client/servers/{server_uuid}/files/rename
```json
{
"summary": "Rename files",
"operationId": "clientRenameFiles",
"description": "Renames one or more files or folders in a server directory.",
"parameters": [],
"responses": {
"204": {
"description": "Files renamed."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
},
"502": {
"description": "Wings could not rename the files.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientRenameFilesRequest"
}
}
}
}
}
```
# Copy file
Creates a copy of a file on the server.
## POST /api/client/servers/{server_uuid}/files/copy
```json
{
"summary": "Copy file",
"operationId": "clientCopyFile",
"description": "Creates a copy of a file on the server.",
"parameters": [],
"responses": {
"204": {
"description": "File copied."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
},
"502": {
"description": "Wings could not copy the file.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientCopyFileRequest"
}
}
}
}
}
```
# Write file contents
Writes the raw request body to a server file. This endpoint does not expect a JSON wrapper for the file contents.
## POST /api/client/servers/{server_uuid}/files/write
```json
{
"summary": "Write file contents",
"operationId": "clientWriteFileContents",
"description": "Writes the raw request body to a server file. This endpoint does not expect a JSON wrapper for the file contents.",
"parameters": [
{
"in": "query",
"name": "file",
"description": "Path to the file to write.",
"required": true,
"schema": {
"$ref": "#/components/schemas/ClientWriteFileContentsFileQueryParameter"
},
"examples": {
"default": {
"value": "/server.properties"
}
}
}
],
"responses": {
"204": {
"description": "File contents written."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
},
"502": {
"description": "Wings could not write the file.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ClientWriteFileContentsRequest"
}
}
}
}
}
```
# Compress files
Creates an archive from one or more files or folders in a server directory.
## POST /api/client/servers/{server_uuid}/files/compress
```json
{
"summary": "Compress files",
"operationId": "clientCompressFiles",
"description": "Creates an archive from one or more files or folders in a server directory.",
"parameters": [],
"responses": {
"200": {
"description": "Archive created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientFileObjectResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
},
"502": {
"description": "Wings could not create the archive.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientCompressFilesRequest"
}
}
}
}
}
```
# Decompress file
Extracts an archive in a server directory.
## POST /api/client/servers/{server_uuid}/files/decompress
```json
{
"summary": "Decompress file",
"operationId": "clientDecompressFile",
"description": "Extracts an archive in a server directory.",
"parameters": [],
"responses": {
"204": {
"description": "Archive extracted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
},
"502": {
"description": "Wings could not extract the archive.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientDecompressFileRequest"
}
}
}
}
}
```
# Delete files
Deletes one or more files or folders from a server directory.
## POST /api/client/servers/{server_uuid}/files/delete
```json
{
"summary": "Delete files",
"operationId": "clientDeleteFiles",
"description": "Deletes one or more files or folders from a server directory.",
"parameters": [],
"responses": {
"204": {
"description": "Files deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
},
"502": {
"description": "Wings could not delete the files.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientDeleteFilesRequest"
}
}
}
}
}
```
# Create folder
Creates a folder in a server directory.
## POST /api/client/servers/{server_uuid}/files/create-folder
```json
{
"summary": "Create folder",
"operationId": "clientCreateFolder",
"description": "Creates a folder in a server directory.",
"parameters": [],
"responses": {
"204": {
"description": "Folder created."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
},
"502": {
"description": "Wings could not create the folder.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientCreateFolderRequest"
}
}
}
}
}
```
# Change file permissions
Updates POSIX mode bits for one or more files or folders in a server directory.
## POST /api/client/servers/{server_uuid}/files/chmod
```json
{
"summary": "Change file permissions",
"operationId": "clientChangeFilePermissions",
"description": "Updates POSIX mode bits for one or more files or folders in a server directory.",
"parameters": [],
"responses": {
"204": {
"description": "File permissions updated."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
},
"502": {
"description": "Wings could not update file permissions.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientChangeFilePermissionsRequest"
}
}
}
}
}
```
# Pull remote file
Requests that Wings download a remote file into a server directory.
## POST /api/client/servers/{server_uuid}/files/pull
```json
{
"summary": "Pull remote file",
"operationId": "clientPullRemoteFile",
"description": "Requests that Wings download a remote file into a server directory.",
"parameters": [],
"responses": {
"204": {
"description": "Remote file pull queued or completed."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
},
"502": {
"description": "Wings could not pull the remote file.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientPullRemoteFileRequest"
}
}
}
}
}
```
# Get file upload URL
Returns a short-lived signed URL for uploading a file directly to Wings.
## GET /api/client/servers/{server_uuid}/files/upload
```json
{
"summary": "Get file upload URL",
"operationId": "clientGetFileUploadURL",
"description": "Returns a short-lived signed URL for uploading a file directly to Wings.",
"parameters": [],
"responses": {
"200": {
"description": "Signed upload URL returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientSignedUrlResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# List server schedules
Returns all schedules configured for the server, including their tasks.
## GET /api/client/servers/{server_uuid}/schedules
```json
{
"summary": "List server schedules",
"operationId": "clientListServerSchedules",
"description": "Returns all schedules configured for the server, including their tasks.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports tasks.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListServerSchedulesIncludeQueryParameter"
},
"examples": {
"default": {
"value": "tasks"
}
}
}
],
"responses": {
"200": {
"description": "Server schedules returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientServerScheduleListResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Create server schedule
Creates a schedule for the server.
## POST /api/client/servers/{server_uuid}/schedules
```json
{
"summary": "Create server schedule",
"operationId": "clientCreateServerSchedule",
"description": "Creates a schedule for the server.",
"parameters": [],
"responses": {
"200": {
"description": "Schedule created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientServerScheduleResource"
}
}
}
},
"400": {
"description": "The cron expression is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientCreateServerScheduleRequest"
}
}
}
}
}
```
# Get server schedule
Returns one schedule configured for the server, including its tasks.
## GET /api/client/servers/{server_uuid}/schedules/{schedule_id}
```json
{
"summary": "Get server schedule",
"operationId": "clientGetServerSchedule",
"description": "Returns one schedule configured for the server, including its tasks.",
"parameters": [
{
"in": "query",
"name": "include",
"description": "Comma-separated relationships to include. Supports tasks.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientGetServerScheduleIncludeQueryParameter"
},
"examples": {
"default": {
"value": "tasks"
}
}
}
],
"responses": {
"200": {
"description": "Server schedule returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientServerScheduleResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Update server schedule
Updates a schedule for the server and recalculates its next run time.
## POST /api/client/servers/{server_uuid}/schedules/{schedule_id}
```json
{
"summary": "Update server schedule",
"operationId": "clientUpdateServerSchedule",
"description": "Updates a schedule for the server and recalculates its next run time.",
"parameters": [],
"responses": {
"200": {
"description": "Schedule updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientServerScheduleResource"
}
}
}
},
"400": {
"description": "The cron expression is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientUpdateServerScheduleRequest"
}
}
}
}
}
```
# Delete server schedule
Deletes a schedule and its tasks from the server.
## DELETE /api/client/servers/{server_uuid}/schedules/{schedule_id}
```json
{
"summary": "Delete server schedule",
"operationId": "clientDeleteServerSchedule",
"description": "Deletes a schedule and its tasks from the server.",
"parameters": [],
"responses": {
"204": {
"description": "Schedule deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Execute server schedule
Queues a schedule to run immediately regardless of its active state.
## POST /api/client/servers/{server_uuid}/schedules/{schedule_id}/execute
```json
{
"summary": "Execute server schedule",
"operationId": "clientExecuteServerSchedule",
"description": "Queues a schedule to run immediately regardless of its active state.",
"parameters": [],
"responses": {
"202": {
"description": "Schedule execution queued."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Create schedule task
Creates a task in a server schedule.
## POST /api/client/servers/{server_uuid}/schedules/{schedule_id}/tasks
```json
{
"summary": "Create schedule task",
"operationId": "clientCreateScheduleTask",
"description": "Creates a task in a server schedule.",
"parameters": [],
"responses": {
"200": {
"description": "Schedule task created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientScheduleTaskResource"
}
}
}
},
"400": {
"description": "The schedule has reached its configured task limit.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientCreateScheduleTaskRequest"
}
}
}
}
}
```
# Update schedule task
Updates a task in a server schedule, including its sequence position.
## POST /api/client/servers/{server_uuid}/schedules/{schedule_id}/tasks/{task_id}
```json
{
"summary": "Update schedule task",
"operationId": "clientUpdateScheduleTask",
"description": "Updates a task in a server schedule, including its sequence position.",
"parameters": [],
"responses": {
"200": {
"description": "Schedule task updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientScheduleTaskResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientUpdateScheduleTaskRequest"
}
}
}
}
}
```
# Delete schedule task
Deletes a task from a server schedule and compacts subsequent sequence IDs.
## DELETE /api/client/servers/{server_uuid}/schedules/{schedule_id}/tasks/{task_id}
```json
{
"summary": "Delete schedule task",
"operationId": "clientDeleteScheduleTask",
"description": "Deletes a task from a server schedule and compacts subsequent sequence IDs.",
"parameters": [],
"responses": {
"204": {
"description": "Schedule task deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# List server allocations
Returns all network allocations assigned to the server.
## GET /api/client/servers/{server_uuid}/network/allocations
```json
{
"summary": "List server allocations",
"operationId": "clientListServerAllocations",
"description": "Returns all network allocations assigned to the server.",
"parameters": [],
"responses": {
"200": {
"description": "Server allocations returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientAllocationListResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Create server allocation
Assigns an available allocation to the server.
## POST /api/client/servers/{server_uuid}/network/allocations
```json
{
"summary": "Create server allocation",
"operationId": "clientCreateServerAllocation",
"description": "Assigns an available allocation to the server.",
"parameters": [],
"responses": {
"200": {
"description": "Allocation assigned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientAllocationResource"
}
}
}
},
"400": {
"description": "The server has reached its configured allocation limit or no allocation is available.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Update allocation notes
Updates notes attached to a server allocation.
## POST /api/client/servers/{server_uuid}/network/allocations/{allocation_id}
```json
{
"summary": "Update allocation notes",
"operationId": "clientUpdateAllocationNotes",
"description": "Updates notes attached to a server allocation.",
"parameters": [],
"responses": {
"200": {
"description": "Allocation notes updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientAllocationResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientUpdateAllocationNotesRequest"
}
}
}
}
}
```
# Delete server allocation
Removes a non-primary allocation from the server and returns it to the available pool.
## DELETE /api/client/servers/{server_uuid}/network/allocations/{allocation_id}
```json
{
"summary": "Delete server allocation",
"operationId": "clientDeleteServerAllocation",
"description": "Removes a non-primary allocation from the server and returns it to the available pool.",
"parameters": [],
"responses": {
"204": {
"description": "Allocation removed."
},
"400": {
"description": "The allocation cannot be removed because it is primary or the server has no allocation limit.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Set primary allocation
Sets the primary network allocation for the server.
## POST /api/client/servers/{server_uuid}/network/allocations/{allocation_id}/primary
```json
{
"summary": "Set primary allocation",
"operationId": "clientSetPrimaryAllocation",
"description": "Sets the primary network allocation for the server.",
"parameters": [],
"responses": {
"200": {
"description": "Primary allocation updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientAllocationResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# List server subusers
Returns subusers assigned to the server.
## GET /api/client/servers/{server_uuid}/users
```json
{
"summary": "List server subusers",
"operationId": "clientListServerSubusers",
"description": "Returns subusers assigned to the server.",
"parameters": [],
"responses": {
"200": {
"description": "Server subusers returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientServerSubuserListResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Create server subuser
Assigns an existing or new user as a subuser on the server.
## POST /api/client/servers/{server_uuid}/users
```json
{
"summary": "Create server subuser",
"operationId": "clientCreateServerSubuser",
"description": "Assigns an existing or new user as a subuser on the server.",
"parameters": [],
"responses": {
"200": {
"description": "Server subuser created.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientServerSubuserResource"
}
}
}
},
"400": {
"description": "The user is already assigned to the server or is the server owner.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientCreateServerSubuserRequest"
}
}
}
}
}
```
# Get server subuser
Returns one subuser assignment for the server.
## GET /api/client/servers/{server}/users/{user}
```json
{
"summary": "Get server subuser",
"operationId": "clientGetServerSubuser",
"description": "Returns one subuser assignment for the server.",
"parameters": [],
"responses": {
"200": {
"description": "Server subuser returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientServerSubuserResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Update server subuser
Updates permissions for a server subuser and revokes their SFTP access tokens.
## POST /api/client/servers/{server_uuid}/users/{user}
```json
{
"summary": "Update server subuser",
"operationId": "clientUpdateServerSubuser",
"description": "Updates permissions for a server subuser and revokes their SFTP access tokens.",
"parameters": [],
"responses": {
"200": {
"description": "Server subuser updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientServerSubuserResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientUpdateServerSubuserRequest"
}
}
}
}
}
```
# Delete server subuser
Removes a subuser assignment from the server and revokes SFTP access tokens.
## DELETE /api/client/servers/{server_uuid}/users/{user}
```json
{
"summary": "Delete server subuser",
"operationId": "clientDeleteServerSubuser",
"description": "Removes a subuser assignment from the server and revokes SFTP access tokens.",
"parameters": [],
"responses": {
"204": {
"description": "Server subuser removed."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# List server backups
Returns a paginated list of backups for the server.
## GET /api/client/servers/{server_uuid}/backups
```json
{
"summary": "List server backups",
"operationId": "clientListServerBackups",
"description": "Returns a paginated list of backups for the server.",
"parameters": [
{
"in": "query",
"name": "page",
"description": "The page number to return.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListServerBackupsPageQueryParameter"
},
"examples": {
"default": {
"value": 1
}
}
},
{
"in": "query",
"name": "per_page",
"description": "Number of backups to return per page. The maximum is 50.",
"required": false,
"schema": {
"$ref": "#/components/schemas/ClientListServerBackupsPerPageQueryParameter"
},
"examples": {
"default": {
"value": 20
}
}
}
],
"responses": {
"200": {
"description": "Server backups returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientBackupPaginatedResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Create server backup
Starts a new backup for the server.
## POST /api/client/servers/{server_uuid}/backups
```json
{
"summary": "Create server backup",
"operationId": "clientCreateServerBackup",
"description": "Starts a new backup for the server.",
"parameters": [],
"responses": {
"200": {
"description": "Backup started.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientBackupResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientCreateServerBackupRequest"
}
}
}
}
}
```
# Get server backup
Returns information about one server backup.
## GET /api/client/servers/{server_uuid}/backups/{backup_uuid}
```json
{
"summary": "Get server backup",
"operationId": "clientGetServerBackup",
"description": "Returns information about one server backup.",
"parameters": [],
"responses": {
"200": {
"description": "Server backup returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientBackupResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Delete server backup
Deletes a backup record and its stored archive.
## DELETE /api/client/servers/{server_uuid}/backups/{backup_uuid}
```json
{
"summary": "Delete server backup",
"operationId": "clientDeleteServerBackup",
"description": "Deletes a backup record and its stored archive.",
"parameters": [],
"responses": {
"204": {
"description": "Backup deleted."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Get backup download URL
Returns a signed URL for downloading a server backup.
## GET /api/client/servers/{server_uuid}/backups/{backup_uuid}/download
```json
{
"summary": "Get backup download URL",
"operationId": "clientGetBackupDownloadURL",
"description": "Returns a signed URL for downloading a server backup.",
"parameters": [],
"responses": {
"200": {
"description": "Signed backup download URL returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientSignedUrlResource"
}
}
}
},
"400": {
"description": "The backup cannot be downloaded from its storage driver.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Toggle backup lock
Toggles whether a backup is locked against deletion.
## POST /api/client/servers/{server_uuid}/backups/{backup_uuid}/lock
```json
{
"summary": "Toggle backup lock",
"operationId": "clientToggleBackupLock",
"description": "Toggles whether a backup is locked against deletion.",
"parameters": [],
"responses": {
"200": {
"description": "Backup lock state toggled.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientBackupResource"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Restore server backup
Restores a backup over the server files.
## POST /api/client/servers/{server_uuid}/backups/{backup_uuid}/restore
```json
{
"summary": "Restore server backup",
"operationId": "clientRestoreServerBackup",
"description": "Restores a backup over the server files.",
"parameters": [],
"responses": {
"204": {
"description": "Backup restore started."
},
"400": {
"description": "The server or backup is not in a restorable state.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientRestoreServerBackupRequest"
}
}
}
}
}
```
# Get startup configuration
Returns the rendered startup command, available Docker images, raw startup command, and user-visible variables for the server.
## GET /api/client/servers/{server_uuid}/startup
```json
{
"summary": "Get startup configuration",
"operationId": "clientGetStartupConfiguration",
"description": "Returns the rendered startup command, available Docker images, raw startup command, and user-visible variables for the server.",
"parameters": [],
"responses": {
"200": {
"description": "Startup configuration returned.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientEggVariableListResponse"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Update startup variable
Updates one editable startup variable for the server and returns the updated rendered startup command.
## PUT /api/client/servers/{server_uuid}/startup/variable
```json
{
"summary": "Update startup variable",
"operationId": "clientUpdateStartupVariable",
"description": "Updates one editable startup variable for the server and returns the updated rendered startup command.",
"parameters": [],
"responses": {
"200": {
"description": "Startup variable updated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientEggVariableResource"
}
}
}
},
"400": {
"description": "The variable does not exist, is hidden, or is read-only.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientUpdateStartupVariableRequest"
}
}
}
}
}
```
# Rename server
Updates the server name and optionally its description.
## POST /api/client/servers/{server_uuid}/settings/rename
```json
{
"summary": "Rename server",
"operationId": "clientRenameServer",
"description": "Updates the server name and optionally its description.",
"parameters": [],
"responses": {
"204": {
"description": "Server renamed."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientRenameServerRequest"
}
}
}
}
}
```
# Reinstall server
Queues a reinstall of the server on Wings.
## POST /api/client/servers/{server_uuid}/settings/reinstall
```json
{
"summary": "Reinstall server",
"operationId": "clientReinstallServer",
"description": "Queues a reinstall of the server on Wings.",
"parameters": [],
"responses": {
"202": {
"description": "Server reinstall queued."
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
]
}
```
# Set server Docker image
Updates the server Docker image to one of the images allowed by the server egg.
## PUT /api/client/servers/{server_uuid}/settings/docker-image
```json
{
"summary": "Set server Docker image",
"operationId": "clientSetServerDockerImage",
"description": "Updates the server Docker image to one of the images allowed by the server egg.",
"parameters": [],
"responses": {
"204": {
"description": "Docker image updated."
},
"400": {
"description": "The server image was manually set by an administrator and cannot be changed through this endpoint.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"401": {
"description": "Authentication credentials were missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"403": {
"description": "The API key does not have permission to perform this action.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"404": {
"description": "The requested resource could not be found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
}
},
"422": {
"description": "The submitted data is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrorEnvelope"
}
}
}
}
},
"tags": [
"Client API"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClientSetServerDockerImageRequest"
}
}
}
}
}
```