Let customers run their servers
Build a Container Apps server panel in your storefront, with power and console, files, snapshots, databases, schedules, ports, software and sharing.
Your storefront's server panel runs on the portal API. It gives a customer the controls that the Container Apps guides describe for the Coritan dashboard, on the servers they bought from you. Every route on this page is under /portal/servers/{uuid}/ or /portal/snapshots/, and takes the customer's token as Authorization: Bearer $CUSTOMER_TOKEN.
Most routes have the same path after {uuid} as the platform's /api/v1/client/servers/{uuid}/ routes and take the same body, so the API sections of the Container Apps guides apply once you change the base path and the token. This page covers what the portal does differently, and the routes those guides do not have.
Before you begin
Section titled Before you begin- Get a customer token as Sign customers in to your storefront describes.
- Find the server's
uuid. It isresource.uuidon the customer's service; see Show the customer's services.
Open a server
Section titled Open a servercurl "https://api.coritan.com/api/v1/orgs/acme/portal/servers/$SERVER_UUID" \
-H "Authorization: Bearer $CUSTOMER_TOKEN"
The answer is the server as the customer should see it: its name, status, plan limits such as memory_mb and disk_mb, and its join_address. On a free server, ip_address and port are empty and players use the join_address. entitlements lists what the plan includes, and one the plan leaves out has allowed: false with a reason and a detail to show in its place. legacy_backup_count says whether the server has backups worth a tab, sleep holds a free server's sleep state, and lock says whether we have locked the server.
Three fields say what this customer may do:
| Field | Meaning |
|---|---|
is_owner |
true for the customer whose service it is, and false for a customer the owner shared it with. |
permissions |
["*"] for the owner. Otherwise, the permissions the owner granted. |
capabilities |
One true or false flag per permission, named with underscores, such as file_read_content. Show or hide each control by these flags. |
GET /resources returns the power state and live use of CPU, memory, disk and network, in the shape Use the console and power controls describes. Poll it while the panel is open. GET /status-ping asks a Minecraft server who is online, and never fails: reachable is false when the game does not answer.
Who can do what
Section titled Who can do whatThe owner can do everything. A customer the owner shared the server with can do what their permissions allow, and a request outside them answers 403 with Insufficient permissions. GET /permissions lists every permission an owner can grant.
| Permission | Lets the customer |
|---|---|
control.console |
Send console commands. |
control.start, control.stop, control.restart |
Send that power signal. kill needs control.stop. Waking and keeping a free server need control.start. |
websocket.connect |
Open the live console. |
file.read, file.read-content |
List files, and read or download them. |
file.create, file.update, file.delete, file.archive |
Upload, create and copy files, or download one from a URL. Edit, rename and change modes. Delete. Compress and extract. |
file.sftp |
Connect over SFTP. |
snapshot.read, snapshot.create, snapshot.restore, snapshot.download, snapshot.delete |
List, take, restore, download and delete snapshots. Locking one needs snapshot.delete. |
backup.read, backup.restore, backup.download, backup.delete |
List, restore, download and delete old backups. Locking one needs backup.delete. |
database.read, database.create, database.update, database.delete |
List databases, create them, rotate their passwords and delete them. |
database.view_password |
See a database's password, and use the SQL editor and table browser. |
schedule.read, schedule.create, schedule.update, schedule.delete |
See schedules and their runs, create them, change them and their tasks or run them now, and delete them. |
allocation.read, allocation.create, allocation.update, allocation.delete |
See ports, add or request one, choose the primary port or publish one, and release one. |
startup.read, startup.update |
See the Java memory and startup flags, and change them and the startup variables. |
settings.rename |
Rename the server and change its join address subdomain. |
settings.reinstall |
Reinstall the server, change its software, install and remove add-ons, and run world optimization. |
Viewing the server, its resources, its sleep state, its software and its custom domain needs no particular permission. Only the owner can share the server, list it in your server directory, and link a custom domain.
Warning
Snapshots belong to the owner's account, not to one server. A customer with snapshot.restore on one server can restore any of the owner's snapshots onto it, including one taken from another server.
Share a server
Section titled Share a serverThe owner shares a server with another customer of your organization by username or email address:
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/servers/$SERVER_UUID/users" \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"username_or_email": "alex@example.com", "permissions": ["control.console", "control.start", "control.stop", "file.read", "file.read-content"]}'
A customer with that username or email gets access at once, and we email them. An email address with no customer behind it gets a pending invite instead, with kind set to invite and an expires_at 14 days away. The invite becomes access when a customer with that address signs in to your storefront.
GET /userslists who has access, each withid,kind(grantorinvite),status,username,emailandpermissions. A shared customer sees the others by username only, without emails or invites.PUT /users/{subuser_id}withpermissionsreplaces what a customer may do.DELETE /users/{subuser_id}removes a customer or cancels an invite, and we email a customer who was removed. A shared customer can remove themselves the same way, or withPOST /users/leave.
Share a server with other users explains each permission in the words the Coritan dashboard uses.
Power and the console
Section titled Power and the consoleSend a power signal with POST /power:
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/servers/$SERVER_UUID/power" \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"signal": "restart"}'
signal is start, stop, restart or kill, and the answer confirms only that we sent it, so read GET /resources for the result. POST /command runs a console command, given as command in the body or in the query string, up to 1,000 characters.
For the live console, GET /websocket returns the socket's address as socket, and the customer's token as token. Open the socket with the token in the token query parameter:
const res = await fetch(`https://api.coritan.com/api/v1/orgs/acme/portal/servers/${uuid}/websocket`, {
headers: { Authorization: `Bearer ${customerToken}` },
});
const { socket, token } = await res.json();
const ws = new WebSocket(`${socket}?token=${encodeURIComponent(token)}`);
ws.onopen = () => ws.send(JSON.stringify({ event: "send logs", args: [null] }));
ws.onmessage = (message) => {
const { event, args } = JSON.parse(message.data);
if (event === "console output") showLine(args[0]);
};
The socket speaks the events that Use the console and power controls lists. Send send command and set state only when the customer holds control.console or the power permission, because the socket refuses them otherwise. When the owner removes a customer's access, the socket closes with code 4003. Do not reopen a socket that closed with 4001, 4003, 4008, 4010 or 4011 until something changes: the token expired, access ended, the session ended, or the server is suspended or gone.
Wake a free server
Section titled Wake a free serverA free server sleeps when nobody plays on it, as How free servers work explains. GET /sleep returns its sleep state: the phase (awake, starting, queued or asleep), sleeps_at to count down from, and the customer's place in the start queue. A paid server answers with policy set to none.
POST /wakeasks for the server to start. Whensleep.challenge_requiredistrue, show the Cloudflare Turnstile challenge first, with the site key fromGET /storefront/branding, and send its answer asturnstile_token.POST /keepmarks a free server as still in use, which resets its count of days without activity. On a server we suspended for inactivity, it also reactivates the server and cancels its deletion. A paid server answers409.
Files and SFTP
Section titled Files and SFTPThe file routes are the ones Manage server files describes, under /files/. Upload with a multipart form, one files part per file:
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/servers/$SERVER_UUID/files/upload?directory=/plugins" \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-F "files=@EssentialsX.jar"
Each file can be up to 100 MB. When some files of a batch fail after others have landed, the answer lists the uploaded names in files and the others in failed. POST /files/pull downloads a file from a URL onto the server, GET /files/pull/status reports on it and POST /files/pull/cancel?download_id=... stops it.
GET /sftp returns the host, port and username to connect with. The username is the customer's own email address and a short server reference, so a shared customer signs in as themselves. The password is the customer's storefront password. A customer who signed up with a social account has none until they set one; see Social sign-in. Connect to a server with SFTP covers the rest.
Take and restore snapshots
Section titled Take and restore snapshotsA snapshot saves the server's files to the account, and can be restored onto any of the owner's servers. The Snapshots guides explain how they work and what the storage allowance counts.
- Check that one fits with
GET /snapshots/estimate. It answersestimated_bytes,remaining_bytes,fitsandshortfall_bytes. The estimate is the server's disk use before compression, so the snapshot is never larger. - Take it:
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/servers/$SERVER_UUID/snapshots" \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Before the 1.21 update", "ignored_files": ["logs"], "is_locked": true}'
name is up to 191 characters, ignored_files leaves out up to 50 paths, and is_locked keeps the snapshot from being rotated away. With no body, the snapshot is called manual-snapshot.
| Route | What it does |
|---|---|
GET /snapshots |
Lists this server's snapshots. scope=account lists every snapshot the owner holds, each marked for whether it can restore onto this server. A shared customer gets this server's list either way. |
POST /snapshots/{snapshot_uuid}/restore |
Restores any of the owner's snapshots onto this server. truncate (default true) deletes the current files first, and allow_mismatch confirms a restore from the same game on another loader. |
GET /snapshots/{snapshot_uuid}/download |
Returns a short-lived download url. |
POST /snapshots/{snapshot_uuid}/lock |
Locks or unlocks a snapshot. |
DELETE /snapshots/{snapshot_uuid} |
Deletes a snapshot. |
GET /snapshots/seed |
For a server ordered from a snapshot, the restore it is waiting on or has finished. POST /snapshots/seed/retry runs a failed one again. |
The account routes under /portal/snapshots/ do the same for the customer's own snapshots without naming a server, so they work after the server has gone. GET /portal/snapshots lists them, GET /portal/snapshots/allowance returns the storage allowance, and GET, DELETE, POST .../lock and GET .../download act on one. POST /portal/snapshots/{snapshot_uuid}/restore needs target_server_uuid, because there is no server in the path to restore onto.
Backups
Section titled BackupsSnapshots have replaced backups, so POST /backups answers 410 with reason set to replaced_by_snapshots. A server that took backups before keeps them: GET /backups lists them, and restore, download, lock and delete work as Manage server backups describes.
Databases
Section titled DatabasesPOST /databases creates a database on the server's database host. name_suffix names it with lowercase letters, digits and underscores, up to 48 characters (default db), and remote limits where it accepts connections from (default %, anywhere). Send an Idempotency-Key header so a retry does not create two.
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/servers/$SERVER_UUID/databases" \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Idempotency-Key: 1f9e6c2a-luckperms" \
-H "Content-Type: application/json" \
-d '{"name_suffix": "luckperms"}'
| Route | What it does |
|---|---|
GET /databases |
Lists the server's databases. |
GET /databases/{database_id}/credentials |
Returns the password and connection details. |
POST /databases/{database_id}/rotate-password |
Sets a new password. |
POST /databases/{database_id}/retry |
Tries a failed creation again. |
DELETE /databases/{database_id} |
Deletes the database. |
POST /databases/{database_id}/query |
Runs the SQL in sql, up to 65,536 characters, as the database's own user. |
GET /databases/{database_id}/tables |
Lists tables. .../tables/{table}/columns describes one. |
GET /databases/{database_id}/tables/{table}/rows |
Reads rows: limit (1–100, default 50), offset, search, sort and direction. |
POST, PATCH /databases/{database_id}/tables/{table}/rows |
Inserts a row from values, or changes the row whose primary key is key. |
POST /databases/{database_id}/tables/{table}/rows/delete |
Deletes the row whose primary key is key. |
GET /databases/{database_id}/tables/{table}/export.csv |
Downloads the table as CSV. X-Export-Truncated: 1 means it holds only part of the table. |
POST .../tables/{table}/truncate, DELETE .../tables/{table} |
Empties or drops a table. |
Create and manage server databases explains the database host and the connection details.
Schedules
Section titled SchedulesSchedules run tasks on a timetable, as Schedule server tasks describes, with the same body under /schedules/:
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/servers/$SERVER_UUID/schedules" \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Nightly restart", "cron_minute": "0", "cron_hour": "4", "cron_day_of_month": "*", "cron_month": "*", "cron_day_of_week": "*", "timezone": "Europe/London", "tasks": [{"action": "command", "payload": {"command": "say Restarting in 60 seconds"}}, {"action": "power", "payload": {"signal": "restart"}, "time_offset": 60}]}'
A shared customer needs schedule.update to change tasks, and also the permission each task's action needs by hand: control.console for a command, the power permission for a power task, and snapshot.create for a snapshot. That way a schedule never does what the customer could not do themselves. GET /schedules/{schedule_uuid}/runs lists recent runs, newest first, and POST /schedules/{schedule_uuid}/execute runs a schedule now.
Ports
Section titled PortsGET /allocations lists the server's ports and how many it may have, and GET /allocations/available-ports lists free ones. POST /allocations adds a port, with an optional port, notes, and on_dedicated_ip to open it on the server's Floating IP. POST /allocations/{allocation_id}/primary makes a port the one players connect to, POST /allocations/{allocation_id}/publish-port publishes one on the game's default port of a Floating IP, and DELETE /allocations/{allocation_id} releases it. Manage server ports explains each.
Your organization can decide that extra ports on free servers go through a request to your staff instead. GET /allocations/request then answers required: true, with the customer's open_request if they have one, and POST /allocations answers 403 with reason set to request_required. Send the request with a note that says what the port is for:
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/servers/$SERVER_UUID/allocations/request" \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"note": "A second port for the web map plugin"}'
note is 3–500 characters. The request opens a conversation in your support inbox tagged port-request, and a second request while one is open returns the first. Your staff add the port as Manage customer servers in the staff console describes.
Software and add-ons
Section titled Software and add-onsThe software routes are the ones Change the server software and the guides after it describe, under /software/. Reading them needs only access to the server. Changing what the server runs, installing, updating and removing add-ons, resource packs, safe mode and Bedrock packs need settings.reinstall, and the Java memory and startup flags at GET and PATCH /software/jvm need startup.read and startup.update.
GET /software/contextsays what the server runs now.GET /software/catalog,.../catalog/{key}/versionsand.../versions/{version}/buildslist what it can change to, andPOST /software/changechanges it. A change that wipes the server needsconfirm_server_name.POST /software/searchfinds add-ons,GET /software/versions/{source_uuid}/{identifier}lists an add-on's versions andGET /software/dependencies/{source_uuid}/{identifier}what one needs.POST /software/installinstalls one;modeset toreplacewipes the server first and needsconfirm_server_name.GET /softwarelists what we installed,GET /software/addonsevery add-on on disk, andGET /software/updatesnewer builds.GET /software/installsfollows install jobs, which.../canceland.../retryact on.POST /software/bedrockwithenabledturns Bedrock crossplay on or off for a Java server.
Import files from another host
Section titled Import files from another hostPOST /import/test-connection and POST /import/start take the body Import a server from another host describes. Testing needs file.read and starting needs file.create. truncate set to true wipes the server first, so it also needs file.delete. GET /import/current returns the running import, GET /import/status/{import_id} reports on one, and DELETE /import/{import_id} stops it.
Change the server's settings
Section titled Change the server's settings| Route | What it does |
|---|---|
PATCH /settings |
name renames the server: 2–48 characters with at least one letter or number. world_optimization_enabled turns world optimization on or off. A free server can turn it on but not off. |
PATCH /subdomain |
Changes the first part of the join address: 3–28 lowercase letters, digits and hyphens, not only digits. |
PATCH /startup-variables |
Changes the startup variables the game allows, as variables. They apply at the next restart. |
POST /reinstall |
Deletes every file and installs the server again. Send the server's name as confirm_server_name; case does not matter. A server whose install never finished needs no name. |
GET /world-optimization |
What removing unused chunks has done to the world, with its recent runs. |
POST /world-optimization |
Runs world optimization now, once a day, on a stopped server with the setting on. |
Link a custom domain
Section titled Link a custom domainThe owner can give the join address a name on their own domain, such as play.example.com.
- Read
GET /custom-domain.expected_cname_targetis the address the customer's DNS record must point at. - Ask the customer to add a CNAME record for their name, pointing at that address. For a domain's root, use a DNS provider that flattens CNAME records or offers ALIAS records.
- Link it:
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/servers/$SERVER_UUID/custom-domain" \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"domain": "example.com", "subdomain": "play", "include_in_list": true}'
subdomain defaults to @, the domain itself. The answer has verification_status: verified when we can see the record, or pending until we can. We keep checking, and POST /custom-domain/verify checks now. When the record stays missing across several checks, the status becomes failed, the name stops working and we email the owner once. It works again by itself when the record returns.
include_in_list shows the custom name in your server directory in place of the join address; change it later with PATCH /custom-domain. DELETE /custom-domain unlinks the name. Changing the server's subdomain moves the address the record must point at, so we email the owner the new one and the name waits as pending until the record moves.
List the server in your directory
Section titled List the server in your directoryWhen your organization runs a public server directory, the owner decides whether their server appears in it. GET /listing returns hidden, the chosen tags, the available_tags and max_tags, and feature_enabled. PATCH /listing with hidden and tags changes them. Listing a server needs at least one of your tags and a join address. Run a community with a server list, forum and guides covers the directory itself.
Result
Section titled ResultYour storefront shows each server with the controls its customer may use, and every request is checked against the owner's grant, so a control your panel shows by mistake still cannot do more than the grant allows.
Troubleshooting
Section titled Troubleshooting401on every call- The customer token is missing, expired or belongs to another organization. See Troubleshooting for customer sign-in.
403withAccess denied- The customer neither owns the server nor has it shared with them. Check the
uuidagainst their services. 403withInsufficient permissions- The owner has not granted the permission this route needs. Hide the control when its
capabilitiesflag isfalse. Only the server owner can manage subusers- A shared customer tried to share the server or change its directory listing. Only the owner can.
409withreasonset toserver_deleting- The server is being deleted. Every route refuses it from then on.
423withreasonset toserver_locked- We locked the server while we review activity that broke our acceptable use policy. Its files stay. Show the
message. Type the server name to confirm a wipe and reinstallconfirm_server_nameis missing or does not match the server's name.409withreasonset toworld_optimization_running- We are removing unused chunks and the server cannot start until that ends. Try again after
retry_after_seconds. File exceeds maximum upload size of 100 MB- Upload the file over SFTP, or download it onto the server from a URL.
Downloading a snapshot is not included on the free plan. …- Free plans leave out snapshot downloads. The customer can still restore the snapshot, or download files one by one from the file routes.
403withreasonset torequest_required- Your organization asks for port requests on free servers. Send one with
POST /allocations/request. Extra ports on this server are added from the Network tab, not requested- Port requests are off for this server. Add the port with
POST /allocations. No matching account in this organization- No customer has that username. Use their email address instead, which sends an invite when they have no account yet.
This server does not sleep; use the power action to start it.- The server is on a paid plan. Start it with
POST /power.