# API & Discord Bots (/docs/panel-guides/api)





import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';

Yes — the XGamingServer panel exposes a full REST API plus a WebSocket for live console. You can use it to build Discord bots, status pages, monitoring tools, or any automation that needs to start/stop your server, push commands, or read uptime, CPU, and RAM in real time.

Our panel is built on **Pterodactyl**, so the **Client API** spec applies directly. This guide walks through the most-asked endpoints with copy-paste examples.

> ⚠️ **Treat your API key like a password.** Anyone with the key can start, stop, or wipe your server. Restrict each key to specific IPs (the create form supports it) and rotate immediately if a key leaks.

***

Get an API Key [#get-an-api-key]

<Steps>
  <Step>
    Sign in at [panel.xgamingserver.com](https://panel.xgamingserver.com) and open your **Account** page.
  </Step>

  <Step>
    Scroll to the **API Key** section.

        <img alt="API Key panel — description field, allowed IPs field, and the list of existing keys with last-used timestamps" src={__img0} placeholder="blur" />
  </Step>

  <Step>
    Enter a description (e.g., `discord-bot`) and — strongly recommended — paste your bot host's IP into **Allowed IPs** (one per line). Leave it blank only if you can't pin a stable IP.
  </Step>

  <Step>
    Click **Create**. Copy the generated key — it starts with `ptlc_` and is shown **only once**. Store it as an environment variable, never in a public repo.
  </Step>
</Steps>

See [Account Settings → API Keys](/docs/panel-guides/account-settings#api-keys) for the full UI walkthrough.

***

Base URL & Authentication [#base-url--authentication]

|                   |                                                                     |
| ----------------- | ------------------------------------------------------------------- |
| **Base URL**      | `https://panel.xgamingserver.com/api/client`                        |
| **Auth header**   | `Authorization: Bearer ptlc_YOUR_API_KEY`                           |
| **Accept header** | `Accept: Application/vnd.pterodactyl.v1+json`                       |
| **Rate limit**    | **240 requests / minute / API key** — check `X-RateLimit-Remaining` |

Every endpoint below is relative to the base URL. Replace `<server-id>` with your server's short identifier — the 8-character string in the panel URL when you open a server (`/server/abc12345`).

Response shape [#response-shape]

Single resource:

```json
{ "object": "server", "attributes": { "...": "..." }, "meta": { } }
```

Collection (paginated):

```json
{
  "object": "list",
  "data": [ { "object": "server", "attributes": { } } ],
  "meta": {
    "pagination": {
      "total": 12, "count": 12, "per_page": 50,
      "current_page": 1, "total_pages": 1, "links": {}
    }
  }
}
```

Common query parameters [#common-query-parameters]

| Param               | Example                             | Purpose                          |
| ------------------- | ----------------------------------- | -------------------------------- |
| `include=`          | `?include=egg,subusers,allocations` | Expand related resources inline  |
| `page` / `per_page` | `?page=2&per_page=50`               | Paginate list responses          |
| `filter[<field>]`   | `?filter[name]=minecraft`           | Filter list results by field     |
| `sort=`             | `?sort=-created_at`                 | Sort (prefix `-` for descending) |

***

Most-Used Endpoints [#most-used-endpoints]

List your servers [#list-your-servers]

```http
GET /api/client
```

Returns every server your account can access. Use this to find each server's `identifier`.

Get server details [#get-server-details]

```http
GET /api/client/servers/<server-id>
```

Returns metadata: name, description, IP, port, allocations, egg variables.

Get live resource utilization (uptime, CPU, RAM) [#get-live-resource-utilization-uptime-cpu-ram]

```http
GET /api/client/servers/<server-id>/resources
```

This is what most Discord bots want. Example response:

```json
{
  "object": "stats",
  "attributes": {
    "current_state": "running",
    "is_suspended": false,
    "resources": {
      "memory_bytes": 1234567890,
      "cpu_absolute": 12.34,
      "disk_bytes": 9876543210,
      "network_rx_bytes": 12345,
      "network_tx_bytes": 67890,
      "uptime": 3600000
    }
  }
}
```

`uptime` is in **milliseconds**. `current_state` is one of `running`, `starting`, `stopping`, `offline`.

Send a power command [#send-a-power-command]

```http
POST /api/client/servers/<server-id>/power
Content-Type: application/json

{ "signal": "start" }
```

Valid signals: `"start"`, `"stop"`, `"restart"`, `"kill"`. Returns **204 No Content** on success.

> Use `kill` only as a last resort — it force-terminates the process and can corrupt saves.

Send a console command [#send-a-console-command]

```http
POST /api/client/servers/<server-id>/command
Content-Type: application/json

{ "command": "say Hello from the bot!" }
```

Returns **204** on success. **The server must be running** — sending a command to an offline server returns 502.

Get WebSocket auth (for live console & log streaming) [#get-websocket-auth-for-live-console--log-streaming]

```http
GET /api/client/servers/<server-id>/websocket
```

Returns:

```json
{
  "data": {
    "token": "eyJ0eXAi...",
    "socket": "wss://node.example.com:8080/api/servers/<uuid>/ws"
  }
}
```

The token expires after 15 minutes — refresh it by re-calling the endpoint.

Files [#files]

| Method | Path                                                  | Purpose                                       |
| ------ | ----------------------------------------------------- | --------------------------------------------- |
| `GET`  | `/servers/<id>/files/list?directory=/`                | List files in a directory                     |
| `GET`  | `/servers/<id>/files/contents?file=server.properties` | Read file contents                            |
| `POST` | `/servers/<id>/files/write?file=server.properties`    | Write file (raw body = file contents)         |
| `GET`  | `/servers/<id>/files/download?file=world.zip`         | Get a one-time signed download URL            |
| `GET`  | `/servers/<id>/files/upload`                          | Get a one-time signed upload URL              |
| `POST` | `/servers/<id>/files/create-folder`                   | Create a directory (`{root, name}`)           |
| `POST` | `/servers/<id>/files/delete`                          | Delete files (`{root, files: [...]}`)         |
| `POST` | `/servers/<id>/files/copy`                            | Copy a file (`{location}`)                    |
| `PUT`  | `/servers/<id>/files/rename`                          | Rename / move (`{root, files: [{from, to}]}`) |
| `POST` | `/servers/<id>/files/compress`                        | Create archive (`{root, files: [...]}`)       |
| `POST` | `/servers/<id>/files/decompress`                      | Extract archive (`{root, file}`)              |

Databases [#databases]

| Method   | Path                                           | Purpose                                  |
| -------- | ---------------------------------------------- | ---------------------------------------- |
| `GET`    | `/servers/<id>/databases`                      | List MySQL databases                     |
| `POST`   | `/servers/<id>/databases`                      | Create a database (`{database, remote}`) |
| `POST`   | `/servers/<id>/databases/<db>/rotate-password` | Rotate the password                      |
| `DELETE` | `/servers/<id>/databases/<db>`                 | Delete the database                      |

Backups [#backups]

| Method   | Path                                      | Purpose                                        |
| -------- | ----------------------------------------- | ---------------------------------------------- |
| `GET`    | `/servers/<id>/backups`                   | List backups                                   |
| `POST`   | `/servers/<id>/backups`                   | Create a backup (`{name, ignored, is_locked}`) |
| `GET`    | `/servers/<id>/backups/<backup>`          | Backup details                                 |
| `GET`    | `/servers/<id>/backups/<backup>/download` | Get a one-time download URL                    |
| `POST`   | `/servers/<id>/backups/<backup>/restore`  | Restore a backup                               |
| `DELETE` | `/servers/<id>/backups/<backup>`          | Delete a backup                                |

Schedules [#schedules]

| Method   | Path                                        | Purpose                  |
| -------- | ------------------------------------------- | ------------------------ |
| `GET`    | `/servers/<id>/schedules`                   | List schedules           |
| `POST`   | `/servers/<id>/schedules`                   | Create a schedule        |
| `GET`    | `/servers/<id>/schedules/<sid>`             | Schedule details         |
| `POST`   | `/servers/<id>/schedules/<sid>`             | Update schedule          |
| `POST`   | `/servers/<id>/schedules/<sid>/execute`     | Manually trigger         |
| `DELETE` | `/servers/<id>/schedules/<sid>`             | Delete schedule          |
| `POST`   | `/servers/<id>/schedules/<sid>/tasks`       | Add a task to a schedule |
| `POST`   | `/servers/<id>/schedules/<sid>/tasks/<tid>` | Update a task            |
| `DELETE` | `/servers/<id>/schedules/<sid>/tasks/<tid>` | Remove a task            |

Subusers [#subusers]

| Method   | Path                         | Purpose                                        |
| -------- | ---------------------------- | ---------------------------------------------- |
| `GET`    | `/servers/<id>/users`        | List subusers                                  |
| `POST`   | `/servers/<id>/users`        | Invite subuser (`{email, permissions: [...]}`) |
| `GET`    | `/servers/<id>/users/<uuid>` | Subuser details                                |
| `POST`   | `/servers/<id>/users/<uuid>` | Update permissions                             |
| `DELETE` | `/servers/<id>/users/<uuid>` | Remove subuser                                 |

Network / allocations [#network--allocations]

| Method   | Path                                              | Purpose                      |
| -------- | ------------------------------------------------- | ---------------------------- |
| `GET`    | `/servers/<id>/network/allocations`               | List allocations             |
| `POST`   | `/servers/<id>/network/allocations`               | Auto-assign a new allocation |
| `POST`   | `/servers/<id>/network/allocations/<aid>`         | Set notes                    |
| `POST`   | `/servers/<id>/network/allocations/<aid>/primary` | Set as primary               |
| `DELETE` | `/servers/<id>/network/allocations/<aid>`         | Release allocation           |

Account [#account]

| Method   | Path                     | Purpose                    |
| -------- | ------------------------ | -------------------------- |
| `GET`    | `/account`               | Account details            |
| `PUT`    | `/account/email`         | Update email               |
| `PUT`    | `/account/password`      | Update password            |
| `GET`    | `/account/two-factor`    | Get 2FA setup QR           |
| `POST`   | `/account/two-factor`    | Enable 2FA (`{code}`)      |
| `DELETE` | `/account/two-factor`    | Disable 2FA (`{password}`) |
| `GET`    | `/account/api-keys`      | List your API keys         |
| `POST`   | `/account/api-keys`      | Create a new key           |
| `DELETE` | `/account/api-keys/<id>` | Revoke a key               |

***

Quick Tests with curl [#quick-tests-with-curl]

Replace `ptlc_xxx` with your key and `abc12345` with your server ID.

```bash
# Server status
curl -H "Authorization: Bearer ptlc_xxx" \
     -H "Accept: application/json" \
     https://panel.xgamingserver.com/api/client/servers/abc12345/resources

# Restart the server
curl -X POST \
     -H "Authorization: Bearer ptlc_xxx" \
     -H "Accept: application/json" \
     -H "Content-Type: application/json" \
     -d '{"signal":"restart"}' \
     https://panel.xgamingserver.com/api/client/servers/abc12345/power

# Send a console command
curl -X POST \
     -H "Authorization: Bearer ptlc_xxx" \
     -H "Accept: application/json" \
     -H "Content-Type: application/json" \
     -d '{"command":"say Backup starting in 5 minutes"}' \
     https://panel.xgamingserver.com/api/client/servers/abc12345/command
```

***

Discord Bot Examples [#discord-bot-examples]

<Tabs items={['Python (discord.py)', 'Node.js (discord.js)']}>
  <Tab value="Python (discord.py)">
    ```python
    import os
    import aiohttp
    import discord
    from discord import app_commands

    PANEL_URL = "https://panel.xgamingserver.com"
    API_KEY = os.environ["XGS_API_KEY"]
    SERVER_ID = os.environ["XGS_SERVER_ID"]

    HEADERS = {
        "Authorization": f"Bearer {API_KEY}",
        "Accept": "application/json",
        "Content-Type": "application/json",
    }

    intents = discord.Intents.default()
    client = discord.Client(intents=intents)
    tree = app_commands.CommandTree(client)


    async def api(method: str, path: str, **kwargs) -> dict | None:
        async with aiohttp.ClientSession(headers=HEADERS) as s:
            async with s.request(method, f"{PANEL_URL}/api/client{path}", **kwargs) as r:
                r.raise_for_status()
                return await r.json() if r.status != 204 else None


    @tree.command(description="Show server status, uptime, and resource use")
    async def status(interaction: discord.Interaction):
        await interaction.response.defer()
        data = await api("GET", f"/servers/{SERVER_ID}/resources")
        a = data["attributes"]
        res = a["resources"]
        uptime_min = res["uptime"] // 60000
        embed = discord.Embed(
            title="Server Status",
            color=0x57F287 if a["current_state"] == "running" else 0xED4245,
        )
        embed.add_field(name="State", value=a["current_state"])
        embed.add_field(name="Uptime", value=f"{uptime_min} min")
        embed.add_field(name="CPU", value=f"{res['cpu_absolute']:.1f}%")
        embed.add_field(name="Memory", value=f"{res['memory_bytes'] / 1024**2:.0f} MB")
        await interaction.followup.send(embed=embed)


    @tree.command(description="Restart the server")
    async def restart(interaction: discord.Interaction):
        await interaction.response.defer(ephemeral=True)
        await api("POST", f"/servers/{SERVER_ID}/power", json={"signal": "restart"})
        await interaction.followup.send("Restart signal sent.", ephemeral=True)


    @tree.command(description="Send a console command to the server")
    async def cmd(interaction: discord.Interaction, command: str):
        await interaction.response.defer(ephemeral=True)
        await api("POST", f"/servers/{SERVER_ID}/command", json={"command": command})
        await interaction.followup.send(f"Sent: `{command}`", ephemeral=True)


    @client.event
    async def on_ready():
        await tree.sync()
        print(f"Bot ready as {client.user}")


    client.run(os.environ["DISCORD_TOKEN"])
    ```

    **Required:** `pip install discord.py aiohttp`

    **Environment variables:** `DISCORD_TOKEN`, `XGS_API_KEY`, `XGS_SERVER_ID`

    **Locking it down:** add a check on `interaction.user.id` (or a role ID) inside the `restart` and `cmd` handlers so only your admins can use them.
  </Tab>

  <Tab value="Node.js (discord.js)">
    ```javascript
    import 'dotenv/config';
    import {
      Client,
      GatewayIntentBits,
      SlashCommandBuilder,
      EmbedBuilder,
      REST,
      Routes,
    } from 'discord.js';

    const PANEL = 'https://panel.xgamingserver.com';
    const { XGS_API_KEY, XGS_SERVER_ID, DISCORD_TOKEN, DISCORD_APP_ID } = process.env;

    async function api(method, path, body) {
      const res = await fetch(`${PANEL}/api/client${path}`, {
        method,
        headers: {
          Authorization: `Bearer ${XGS_API_KEY}`,
          Accept: 'application/json',
          'Content-Type': 'application/json',
        },
        body: body ? JSON.stringify(body) : undefined,
      });
      if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
      return res.status === 204 ? null : res.json();
    }

    const commands = [
      new SlashCommandBuilder().setName('status').setDescription('Server status'),
      new SlashCommandBuilder().setName('restart').setDescription('Restart server'),
      new SlashCommandBuilder()
        .setName('cmd')
        .setDescription('Send a console command')
        .addStringOption(o => o.setName('command').setDescription('Command').setRequired(true)),
    ].map(c => c.toJSON());

    const rest = new REST().setToken(DISCORD_TOKEN);
    await rest.put(Routes.applicationCommands(DISCORD_APP_ID), { body: commands });

    const client = new Client({ intents: [GatewayIntentBits.Guilds] });

    client.on('interactionCreate', async i => {
      if (!i.isChatInputCommand()) return;

      if (i.commandName === 'status') {
        await i.deferReply();
        const { attributes: a } = await api('GET', `/servers/${XGS_SERVER_ID}/resources`);
        const r = a.resources;
        const embed = new EmbedBuilder()
          .setTitle('Server Status')
          .setColor(a.current_state === 'running' ? 0x57f287 : 0xed4245)
          .addFields(
            { name: 'State', value: a.current_state, inline: true },
            { name: 'Uptime', value: `${Math.floor(r.uptime / 60000)} min`, inline: true },
            { name: 'CPU', value: `${r.cpu_absolute.toFixed(1)}%`, inline: true },
            { name: 'Memory', value: `${Math.round(r.memory_bytes / 1024 ** 2)} MB`, inline: true },
          );
        await i.editReply({ embeds: [embed] });
      }

      if (i.commandName === 'restart') {
        await i.deferReply({ ephemeral: true });
        await api('POST', `/servers/${XGS_SERVER_ID}/power`, { signal: 'restart' });
        await i.editReply('Restart signal sent.');
      }

      if (i.commandName === 'cmd') {
        await i.deferReply({ ephemeral: true });
        const command = i.options.getString('command');
        await api('POST', `/servers/${XGS_SERVER_ID}/command`, { command });
        await i.editReply(`Sent: \`${command}\``);
      }
    });

    client.login(DISCORD_TOKEN);
    ```

    **Required:** `npm i discord.js dotenv`

    **Environment variables:** `DISCORD_TOKEN`, `DISCORD_APP_ID`, `XGS_API_KEY`, `XGS_SERVER_ID`

    **Locking it down:** check `i.member.roles.cache.has('YOUR_ADMIN_ROLE_ID')` before executing `restart` or `cmd`.
  </Tab>
</Tabs>

***

Live Console via WebSocket [#live-console-via-websocket]

For live log streaming and chat-relay bots, use the WebSocket. The flow:

1. `GET /api/client/servers/<id>/websocket` to get a `token` and `socket` URL
2. Connect to the `socket` URL with a normal WebSocket client
3. Send `{"event":"auth","args":["<token>"]}` immediately after connect
4. Listen for these events:
   * `console output` — each line from the server console (`args[0]` is the line)
   * `stats` — periodic resource snapshots (JSON)
   * `status` — power state changes
   * `token expiring` — re-fetch the websocket endpoint and send `auth` again
   * `token expired` — connection will close; reconnect with a new token

Send commands or power signals over the same socket:

```json
{"event":"send command","args":["say Hello from WebSocket"]}
{"event":"set state","args":["restart"]}
```

***

Rate Limiting & Best Practices [#rate-limiting--best-practices]

* **240 requests/min/key.** Cache responses where you can — a status command polled once per second from many users will burn the budget fast.
* **Watch the headers.** `X-RateLimit-Remaining` and `X-RateLimit-Reset` (Unix timestamp) are returned on every response.
* **Use the WebSocket** for anything that needs frequent updates — it doesn't count against the REST limit.
* **One key per app.** Don't reuse the same key across a Discord bot, a status page, and a CI script. If one leaks, you can revoke without breaking the others.
* **IP allowlist when possible.** Add the bot host's IP at key creation time. Even if the key leaks, attackers can't use it from elsewhere.
* **Handle 5xx with backoff.** Game nodes occasionally restart — exponential backoff and retry, don't hammer.

***

Troubleshooting [#troubleshooting]

**`401 Unauthorized`**

* Missing or wrong `Authorization` header. Verify the key starts with `ptlc_` and the header is `Bearer <key>`.
* Key was revoked or the account it belongs to lost server access.

**`403 Forbidden`**

* Your account doesn't have permission for that endpoint on this server (subuser with limited perms, suspended server).

**`404 Not Found`**

* Wrong `<server-id>`. Use `GET /api/client` to confirm the identifier.

**`429 Too Many Requests`**

* You hit the 240/min cap. Back off until `X-RateLimit-Reset` and reduce poll frequency.

**`502 Bad Gateway` on `/command`**

* The server is offline. Start it first, then send commands.

**WebSocket disconnects after 15 minutes**

* Token expired. Listen for the `token expiring` event and refresh by re-calling `/websocket`.

***

Related Pages [#related-pages]

* [Account Settings → API Keys](/docs/panel-guides/account-settings#api-keys)
* [Console](/docs/panel-guides/console)
* [Server Management](/docs/panel-guides/server-management)
* [Subusers](/docs/panel-guides/subusers) — share API access without sharing your password
