---
title: Pterodactyl
description: "Pterodactyl is RackHelm's game-server integration. It creates panel users and servers from paid orders, resizes them when a customer buys an upgrade, and gives customers a live console, power controls and startup-variable editing inside the client portal — without a…"
url: "https://rackhelm.com/documentation/pterodactyl-integration/"
updated: "2026-09-24"
category: "Integrations: Control Panels"
---

# Pterodactyl

Pterodactyl is RackHelm's game-server integration. It creates panel users and servers from paid orders, resizes them when a customer buys an upgrade, and gives customers a live console, power controls and startup-variable editing inside the client portal — without a second login. This article covers the connection, the per-product configuration, how the deploy location is really chosen, and the operational traps that catch most installs. It is written for whoever sets up and looks after the integration, not for customers.

## Before you start

- The `edit_integrations` permission. See [Roles & Permissions](/docs/roles-and-permissions/).
- A Pterodactyl panel reachable from RackHelm over the public internet with a certificate that validates.
- An **Application API key** (**Admin → Application API**, the `ptla_` key) with read and write on **Users**, **Servers**, **Nodes** and **Nests** — RackHelm calls all four.
- A **Client API key** from a panel account (**Account → API Credentials**, the `ptlc_` key), which drives the in-portal console, power actions and live stats. RackHelm uses this one shared account for every customer.

## Add the connection

1. Open **System → Integrations** and press **Configure** on the **Pterodactyl** card. The button reads **Manage** once the panel has been saved at least once.
2. Set **Base URL** to the panel origin, with no path — for example `https://panel.example.com`.
3. Paste the application key into **Application API Token** and the client key into **Client API Token**.
4. Tick **Active**.
5. Press **Save**, then use **Test Application API** and **Test Client API**. The first asks the panel for one application user; the second reads the account behind the client key and reports only whether it worked — the panel's answer is never echoed back into the browser.

![The Pterodactyl panel form with Base URL, Application API Token, Client API Token, Server Defaults and an Allowed Origins textarea](/wp-content/uploads/rackhelm-docs/integrations-core-panel-edit-pterodactyl.png)
Pterodactyl is the only panel with two tokens and two test buttons.

### Fields

| Field | Rule | Notes |
| --- | --- | --- |
| **Name** | Optional, max 120 characters | Defaults to "Pterodactyl". |
| **Active** | Checkbox | Required for Pterodactyl to appear as an Integration Type, and for the Nest/Egg/Node lookups on a product to answer at all — they return 404 for an inactive panel. |
| **Base URL** | Required, http/https, publicly resolvable | Re-validated on every outbound call, not only on save. The connection is then pinned to the address that passed validation, and redirects are refused, so the token cannot be replayed to somewhere else. |
| **Application API Token** | Optional on edit | The `ptla_` key. Blank keeps the stored token. Stored encrypted. |
| **Client API Token** | Optional on edit | The `ptlc_` key. Blank keeps the stored token. Stored encrypted. |
| **Owner Panel User ID** | Integer, minimum 1 | Stored, but not read when a server is created — set the owner on the product instead, or leave every customer with their own panel user. |
| **Service Panel User ID** | Integer, minimum 1 | Stored only. Earlier releases fell back to it when a product named no owner; the current driver does not. |
| **Default Location ID** | Integer, minimum 1, pre-filled with 1 | Stored only. It is *not* the deploy location — see [Where the deploy location comes from](#location). |
| **Allowed Origins** | One origin per line | Split into a list and saved, but RackHelm does not act on it — it mirrors what you must set in Wings. See [Wings allowed origins](#wings). |

> **Tip:** Re-type both tokens before pressing either Test button. A blank token box makes the browser send the literal word `KEEP`, and neither Pterodactyl tester swaps that for the stored value — unlike the GameCP tester, which does.

## Bind a product to Pterodactyl

Edit the product under **Catalogue → Catalogue & Groups**, open the **Provisioning** tab and choose **Pterodactyl**. Pick the **Panel** first — the **Nest**, **Egg** and **Node** dropdowns are loaded live from that panel with the application token, and stay empty until a panel is selected.

![The Pterodactyl section of a product's Provisioning tab showing Nest, Egg and Node selectors, client permissions, resource limits and an environment variable list](/wp-content/uploads/rackhelm-docs/integrations-core-ptero-product-config.png)
Nest, Egg and Node are fetched from the live panel, so a mis-typed token shows up here as "(failed to load)".

| Field | What it does |
| --- | --- |
| **Nest** / **Egg** | Required. The egg supplies the default Docker image, startup command and variables. RackHelm fetches the egg on every provision — even when you override the image and startup command — so required variables such as `STEAM_ACC` are never dropped. |
| **Node** | Saved with the product, but the create call deploys by *location*, not node — see the warning below. |
| **Owner User ID** | Optional. A panel user that owns every server sold from this product. Leave empty to give each customer their own. |
| **Use Client Email** | On by default. When off, the panel user is created as `{customer id}@{your portal hostname}` instead of the customer's real address. |
| **Service Configuration** | What the customer may change in the portal: **None**, **Nest and Egg**, or **Egg Only**. |
| **Edit Startup Variables** / **Edit Docker Image** / **Edit Startup Command** | Individual permissions for the customer's server page. All off by default. |
| **Docker Image** / **Startup Command** | Optional overrides. Left blank, the egg's own values are used. |
| **Memory (MB)** | Minimum 64. Defaults to 1024 if left blank. |
| **Disk (MB)** | Minimum 100. Defaults to 10240. |
| **CPU (%)** | Minimum 5. Defaults to 100. |
| **Swap (MB)** | Minimum −1. Defaults to 0. |
| **I/O Weight** | 10–1000, default 500. |
| **DB Limit** / **Allocations** / **Backups** | Feature limits, all default 0. |
| **Environment (key → value)** | Extra variables layered over the egg's defaults. Use **Add Variable** to add rows. |
| **Start server after provisioning** | On by default and stored with the product, but not read — the create call always asks the panel to start the server. |

> **Note:** The resource limits here are the *base package*, not a ceiling. A paid upgrade add-on is meant to exceed them, so a value bought by the customer is never clamped back down to the figure on this tab. If you need a genuine per-product maximum, it has to be enforced when the item is added to the cart, not here.

## Where the deploy location comes from

This is the single most common reason a Pterodactyl product refuses to provision, because there is no location field anywhere on the **Provisioning** tab to fill in.

Deployment is by location, not node. RackHelm sends one location ID in the create call's `deploy.locations` array and lets the panel pick a node inside it; the **Node** you chose on the product is stored but never sent. The location comes from a variable named `LocationID`, resolved in this order:

| # | Source | Notes |
| --- | --- | --- |
| 1 | `LocationID` in the service's own order item variables | What the customer picked at checkout, when `LocationID` is offered as a Configurable Option. Matched on the exact key `LocationID`, letter for letter. |
| 2 | A **product add-on** assigned to the **product**, linked to a custom field keyed `LocationID` | Its *first option by sort order* is used as the default. The key match here ignores letter case. |
| 3 | The same, assigned to the product's **group** | Only consulted when the product itself has no such add-on. |
| 4 | `location` in the integration config | Last-resort fallback. No screen in the admin writes this key, so in practice it is only present on rows created some other way. |

If none of the four yields a value, provisioning fails with:

```bash
Missing LocationID (set via product/group variable or config.location)
```

### Setting it up

1. Go to **Catalogue → Custom Fields** and create a field. Set **Display Name** to something readable such as "Server Location", and **System Key** to exactly `LocationID`. The help text under that box — "Used by integrations to identify this value. Must be unique." — is describing precisely this. Choose the **Dropdown** field type and add one option per panel location: the **Label** is what the customer reads, the **Value** is the numeric Pterodactyl location ID.
2. Go to **Catalogue → Product Addons** and create an add-on. Set **Linked Custom Field** to the field you just made — the form's own note says "Link this addon to a server variable (e.g. Operating System or Location) it should set." Give it the same options, each with its location ID as the **Value**. Put the location you want as the default first in the list.
3. Edit the product, open the **Options & Add-ons** tab and tick the add-on under **Available Add-ons**. Assign it to the product group instead if every product in that group deploys the same way.
4. To let customers choose, also tick the custom field under **Configurable Options** on the same tab and leave **Show on Order Page** and **User Changeable** on. Turn **User Changeable** off and the buyer's value is discarded, falling back to the add-on's first option.

![The Product Addon edit screen with Linked Custom Field set to a location field and option rows holding a label and a numeric value](/wp-content/uploads/rackhelm-docs/integrations-core-ptero-locationid-addon.png)
The add-on's Linked Custom Field is what ties its options to the `LocationID` variable. The first option by sort order becomes the product's default location.

![A product's Options and Add-ons tab, with Configurable Options on the left and a list of tickable Available Add-ons on the right](/wp-content/uploads/rackhelm-docs/integrations-core-product-options-addons.png)
Configurable Options put the field on the order page; Available Add-ons is what the location resolver actually searches.

> **Warning:** The two halves are separate, and only ticking the field under **Configurable Options** is not enough. Without an add-on assignment there is no default at all, so every order where the customer leaves the field blank fails with "Missing LocationID (set via product/group variable or config.location)" — and it fails at provisioning time, after the money has been taken.

> **Warning:** Selling the location as a paid add-on on its own does not steer deployment. An add-on option the customer chooses is recorded against the add-on's own order item, while the location is read from the parent service's item, so the server still deploys to the add-on's first option. Offer the location as a Configurable Option if the customer is to pick it.

## What happens on provision

1. RackHelm pings `/api/application/nodes` to confirm the panel is answering.
2. It resolves the owner: the product's **Owner User ID**, else a panel user already linked to this customer, else a panel user looked up by email, else a new one created from their name and email with the RackHelm user ID recorded as the panel's `external_id`.
3. It resolves the location as above, then fetches the egg with its variables for the Docker image, startup command and variable defaults.
4. The service's own variables and those of any participating add-on children are merged; keys prefixed `environment.` have the prefix stripped and go straight into the server's environment.
5. The server is created and its UUID, numeric ID, short identifier, node, egg, nest and location are stored on the order item. Add-on children are stamped with the parent's server ID and marked provisioned; if the create fails they are all marked failed with the parent's error.

### Server names

A new server is named `<product slug>-<12 hex characters of the order item id>`, for example `minecraft_basic-9f3c41a0b2d7`. Before 1.0.1.1 the name was built by printing the order item id as a decimal number; because that id is a UUID, every server on the panel came out as `minecraft-1` or `minecraft-19` and they were impossible to tell apart.

> **Note:** Servers created before the upgrade keep the names the panel already holds. Nothing in RackHelm looks a server up by name — every lookup goes through the stored server ID — so the old names are cosmetic and are deliberately not rewritten. Rename them in the panel if the duplicates bother you.

## Resource add-ons and upgrades

An add-on changes a server's limits by carrying a variable whose key matches the limit it should set. Create the custom field, link it to the add-on and assign the add-on to the product exactly as for `LocationID`; only the key changes.

| Variable key | Pterodactyl limit |
| --- | --- |
| `ServerMemory` | memory (MB) |
| `ServerSwap` | swap (MB) |
| `ServerDisk` | disk (MB) |
| `ServerCPU` | cpu (%) |
| `ServerIO` | io weight |
| `ServerThreads` | threads |
| `ServerBackups` | feature limit: backups |
| `ServerDatabases` | feature limit: databases |
| `ServerAllocations` | feature limit: allocations |

Add-on values *replace* the base package figure rather than adding to it, because an upgrade is expected to exceed the base. They are applied twice: once when the server is created, and again by a recalculation job that recomputes every limit from the base package plus all active add-on children and pushes the result to the panel's server build endpoint. That job runs when an add-on's invoice is paid, when an add-on is removed or terminated, and when a package change completes.

> **Warning:** Before 1.0.1.1 the recalculation job mapped `ServerSwap` onto the disk limit and `ServerDisk` onto the backup count. A disk upgrade therefore changed how many backups the customer could keep and left the disk untouched, and a swap upgrade silently resized the disk. Both now map to the limit of the same name. Servers resized while the defect was live keep whatever the panel was told at the time — check the disk, swap and backup figures on any server that bought one of those add-ons, and re-save the add-on to push a corrected set of limits.

> **Note:** The two application points do not agree about empty values. At create time a blank or zero add-on value is ignored and the base package figure stands. At recalculation, a zero is ignored for memory, swap and CPU but honoured for disk, databases, allocations, I/O, threads and backups — and zero means unlimited to Pterodactyl, not nothing. Give options a real number unless you mean unlimited.

> **Note:** At create time only add-ons with **Needs provisioning** ticked contribute their variables. The recalculation job is not that fussy — it reads every active add-on child. An add-on without that flag therefore does nothing until the next recalculation, and then takes effect.

## Suspension, unsuspension and termination

Suspend, unsuspend and terminate map to the panel's own server actions. Renewal is billing-only and sends nothing to the panel. Suspension runs automatically when an invoice passes the overdue threshold; unsuspension runs when that invoice is paid.

> **Warning:** Pterodactyl answers suspend, unsuspend and reinstall with `204 No Content`. Before 1.0.1.1 RackHelm treated that empty body as a failure *after* the panel had already acted, so the service was genuinely suspended on the panel while its provisioning status was set to `failed` and an error recorded against it. Those items are still marked failed after you upgrade and will sit on **Pending Provisioning** until something clears them. The safe fix is **Mark Provisioned** on that page, or paying the invoice so the unsuspend runs and rewrites the status.

> **Warning:** Do not press **Retry Provisioning** on one of those items. Retry queues a fresh provisioning run, and that run checks only the item's status — not whether a server already exists — so it creates a second server on the panel and overwrites the stored server ID with the new one, orphaning the first.

## The in-portal console

A provisioned Pterodactyl service appears under **Servers** in the client portal, with a live console, power buttons, a command box, resource graphs and — where you have allowed it — startup-variable and Docker-image editing. Reinstalling a server and saving startup changes both require the customer to re-enter their password.

### Wings allowed origins (per node)

The console does not go through the panel. Pterodactyl hands the browser a URL pointing directly at the Wings daemon on the node hosting the server, so **every node** must accept the portal as an origin. On each node, edit `/etc/pterodactyl/config.yml`:

```bash
allowed_origins:
  - https://portal.example.com
```

Restart Wings afterwards.

RackHelm handles its own side automatically. The server page's content-security policy is widened for that one response with every node's daemon origin, read from the panel's node list and cached for an hour, plus whatever origin the last successful handshake for that service actually returned, cached for a day. Both the `https://` and the `wss://` form of each origin are emitted, because the policy does not let an `https://` source match a `wss://` request. A node added later is picked up within the hour, and the first console fetch on a server records the true origin even if the node list was unavailable.

> **Warning:** Miss the `allowed_origins` entry on one node and only servers on *that* node have a dead console and flat resource graphs. It is not an all-or-nothing failure, so check every node.

> **Note:** A node whose FQDN in the panel is not a bare hostname — anything with a space, comma or semicolon in it — is dropped from the policy rather than escaped, because those characters would let it append directives of its own to the header. Consoles on that node will not connect until the FQDN is corrected in Pterodactyl.

### The websocket rate limit

Pterodactyl throttles `GET /api/client/servers/{id}/websocket` far harder than the rest of the client API — roughly **5 requests a minute** against 256 for everything else — and RackHelm talks to the panel as a single shared account, so every customer's console draws on the same five. A browser reconnect loop can exhaust that on its own.

RackHelm mitigates this rather than working around it. The handshake token is a short-lived JWT that Wings accepts across reconnects and tabs, so it is cached and only refreshed once it is within two minutes of expiring. If that refresh is itself throttled, the still-valid cached token is served rather than killing the console. A genuine 429 is passed to the browser with the panel's own `retry_after` value so it waits instead of retrying on a fixed timer, and the customer sees "The game panel is rate limiting us. Retrying in 60s."

Restarting Wings invalidates every token issued before it booted. The browser reports that and asks for a fresh one, which is honoured at most once every ten seconds per server — enough to recover, not enough for a looping client to burn the panel's whole handshake budget.

> **Tip:** If consoles across the whole portal go quiet at once, suspect the panel's websocket throttle rather than any one customer — and check whether Wings was restarted in the last few minutes.

## Troubleshooting

| Symptom | Cause and fix |
| --- | --- |
| "Missing LocationID (set via product/group variable or config.location)" | No `LocationID` is available. Assign an add-on linked to a `LocationID` custom field to the product or its group, and check the System Key is spelled exactly that way. See [Where the deploy location comes from](#location). |
| "Missing Pterodactyl panel credentials (base_url/application_token)" | The panel row has no `ptla_` application key. A client key is deliberately not accepted as a substitute — it can never call `/api/application`, and accepting it would only turn this clear message into a 403 at the first request. Re-enter the Application API Token and save. |
| Order fails with "Unable to reach Pterodactyl API at …" while **Test Application API** passes | The ping runs against `/api/application/nodes` with the stored token, not the one you just typed into the form. Check the token was actually saved, that the panel is marked **Active**, and the application log for the real status. |
| "Pterodactyl base URL failed SSRF safety checks." | The base URL resolves to a private, reserved or unresolvable address. Private control panels have to be listed in `SAFEURL_PRIVATE_ALLOWLIST`. See [Adding a control panel connection](/docs/adding-a-panel-connection/). |
| "Config missing nest and/or egg (provide 'nest' & 'egg' IDs)." | No Nest/Egg selected on the product. They populate only after you pick a Panel. |
| Nest, Egg or Node dropdown shows "(failed to load)" | The lookup calls the panel with the application token. Check the token, and that the panel connection is marked **Active** — the lookup endpoints return 404 for an inactive panel. |
| "Egg defaults unavailable: missing docker_image/startup." | The nest/egg pair does not exist on the panel any more, or the token cannot read nests. The message names the nest and egg it tried. Re-pick the egg. |
| "Pterodactyl server create failed: …" | The panel's own error, passed through. Usually no capacity in the chosen location, or a missing egg variable. |
| "Failed to create Pterodactyl user for email …" | The application key cannot create users. Give it write access to Users. |
| A service shows as failed but the server is suspended on the panel | It was suspended before 1.0.1.1, when a `204 No Content` answer was read as a failure. Use **Mark Provisioned** on Pending Provisioning — never **Retry Provisioning**, which builds a second server. |
| A disk or swap upgrade changed the wrong figure | The pre-1.0.1.1 mapping defect. Check disk, swap and backups on the panel and re-save the add-on to push corrected limits. |
| Console dead, resource graphs flat, browser blocks a `wss://` connection | The node's Wings `allowed_origins` does not list your portal URL. Fix it on that node and restart Wings. |
| Console shows a rate-limit message | The panel's 5-per-minute websocket throttle. Wait for the `retry_after` window; avoid opening many console tabs at once. |
| Console reconnects endlessly right after a Wings restart | Wings rejects tokens issued before it booted. RackHelm fetches a fresh one automatically; allow up to 10 seconds per server. |
| "Unauthenticated: Pterodactyl rejected this Key. Please ensure you are using a full 48-character Client API Key." | The Client API Token is wrong, truncated, or an application key has been pasted into it. |
