Cloud API
The Colyseus Cloud API gives read-only access to what the dashboard shows: your applications’ status, metrics, egress, deploys and logs, and your billing summary. Use it to feed your own dashboards or monitoring tools.
Beta. Every team can use the API. Tell us what you’d like it to cover at support@colyseus.io.
Quickstart
Create a token
Create a token under API tokens in the dashboard (open your API tokens). It’s shown only once, so keep it somewhere safe.
Run this, paste the token and press Enter. It keeps the token out of your shell history:
read -rs COLYSEUS_CLOUD_TOKEN && export COLYSEUS_CLOUD_TOKENFind your application’s slug
curl https://cloud-prod.colyseus.io/api/v1/applications \
-H "Authorization: Bearer $COLYSEUS_CLOUD_TOKEN"Each application’s slug, such as 1234-my-game, goes in the URLs of the other endpoints.
Read its current metrics
curl https://cloud-prod.colyseus.io/api/v1/applications/1234-my-game/metrics/current \
-H "Authorization: Bearer $COLYSEUS_CLOUD_TOKEN"Authentication
Owners and admins create tokens under API tokens in the dashboard. A token can read every application of the team and the team’s billing summary, and nothing else: it cannot deploy or change settings.
A token is shown only once. It doesn’t expire, but it stops working when the person who created it leaves the team, is removed from it, or stops being an owner or admin. Any owner or admin can also delete it, which revokes it immediately.
Send the token in the Authorization header, as Bearer <token>.
Endpoints
The base URL is https://cloud-prod.colyseus.io/api/v1. All times are ISO 8601 in UTC. Applications are addressed by their slug, as returned by /applications. The slug stays the same when you rename the application.
| Endpoint | Returns |
|---|---|
GET /applications | Your applications, with their regions, plans and instances. |
GET /applications/{slug}/metrics/current | Each instance’s latest CCU, rooms, CPU, memory and processes. |
GET /applications/{slug}/metrics | The same per location, by minute or by day. |
GET /applications/{slug}/egress | Network traffic per location and per day. |
GET /applications/{slug}/deploys | Recent deploys, and each one’s output. |
GET /billing | Your subscriptions and account balance. |
GET /applications/{slug}/instances/{id}/logs | An instance’s latest server logs. |
List applications
GET /api/v1/applications
Your team’s applications, with their locations (region, endpoint, plan) and the instances in each location.
{
"data": [{
"id": 1234,
"slug": "1234-my-game",
"name": "My Game",
"maintenance_mode": false,
"created_at": "2026-08-01T09:30:00Z",
"locations": [{
"id": 567,
"region": { "code": "fra", "city": "Frankfurt", "country": "DE" },
"status": "ready",
"scale_strategy": "vertical",
"endpoint": "https://de-fra-1a2b3c4d.colyseus.dev",
"plan": { "vcpus": 1, "ram_mb": 1024, "transfer_gb": 1024 },
"instances": [{
"id": 890,
"endpoint": "https://de-fra-1a2b3c4d.colyseus.dev",
"status": "ready",
"deploy_status": "deployed",
"deployed_at": "2026-09-24T14:02:11Z"
}]
}]
}]
}Use slug in every application URL below, and an instance’s id to read its logs.
scale_strategy is vertical (one instance) or horizontal (several instances behind a load balancer). A location’s or instance’s status is one of pending, provisioning, installing, ready, error, destroying, resizing, unpaid or parked (an application that hasn’t deployed in its first month has its server removed until its first deploy). An instance’s deploy_status takes the deploy values, or is null. endpoint is null until the location or instance has an address.
maintenance_mode is true while the application’s maintenance mode is on in the dashboard. plan.transfer_gb is the monthly transfer the plan includes per instance; Egress has your usage.
Current metrics
GET /api/v1/applications/{slug}/metrics/current
Returns each instance’s latest sample (instances report once a minute) and totals per location. In the totals, CCU and rooms are summed, while CPU and memory are the busiest instance’s. An instance that hasn’t reported in the last 5 minutes has null values and is left out of the totals. A location whose instances all went quiet has null totals.
{
"application": "1234-my-game",
"window_minutes": 5,
"locations": [{
"id": 567,
"region": "fra",
"totals": { "ccu": 18, "rooms": 4, "cpu_percent": 12.5, "memory_percent": 41.2 },
"instances": [{
"id": 890,
"sampled_at": "2026-09-25T10:41:00Z",
"ccu": 18,
"rooms": 4,
"cpu_percent": 12.5,
"memory_percent": 41.2,
"processes": [{
"id": 0,
"status": "online",
"started_at": "2026-09-24T14:02:05Z",
"restarts": 0,
"cpu_percent": 12.5,
"memory_mb": 210.4,
"ccu": 18,
"rooms": 4,
"node_version": "24.1.0"
}]
}]
}]
}memory_percent is relative to the plan’s RAM (plan.ram_mb in /applications).
processes lists the instance’s server processes as they last reported, like the Stats page: status is online, launching, stopping, stopped, waiting restart or errored, and restarts is the process manager’s restart counter for it. It is empty when the instance hasn’t reported in the last 5 minutes.
Metrics history
GET /api/v1/applications/{slug}/metrics
| Parameter | Description |
|---|---|
resolution | minute (default) or day. |
from, to | ISO 8601 times. One without an offset is read as UTC. Defaults to the last 24 hours (minute) or the last 30 days (day). A minute window can span at most 24 hours. |
location_id | Only this location. |
Each point covers one location, aggregated like the current totals: CCU and rooms are summed across instances, CPU and memory are the busiest instance’s. A day point adds up each instance’s own peak of that day. Its CCU can therefore exceed what the location reached at any single moment.
With resolution=day, from and to are whole days (a time of day is ignored), and from, to and each point’s time are midnight UTC. to is inclusive at both resolutions.
Metrics are kept for 35 days. A from further back returns a 422.
{
"application": "1234-my-game",
"resolution": "minute",
"from": "2026-09-24T10:41:00Z",
"to": "2026-09-25T10:41:00Z",
"locations": [{
"id": 567,
"region": "fra",
"points": [
{ "time": "2026-09-24T10:41:00Z", "ccu": 20, "rooms": 5, "cpu_percent": 14.1, "memory_percent": 40.8 }
]
}]
}Egress
GET /api/v1/applications/{slug}/egress
Network traffic per location and per day, summed across its instances.
| Parameter | Description |
|---|---|
from, to | Dates (YYYY-MM-DD). Defaults to the last 30 days. Egress is kept for 35 days. |
{
"application": "1234-my-game",
"from": "2026-08-31",
"to": "2026-09-29",
"collected_at": "2026-09-29T15:20:04Z",
"locations": [{
"id": 567,
"region": "fra",
"outgoing_bytes": 48318382080,
"incoming_bytes": 5368709120,
"days": [
{ "date": "2026-09-28", "outgoing_bytes": 1610612736, "incoming_bytes": 178956970 }
]
}]
}We copy these figures from our infrastructure provider every hour; collected_at says when, and is null before the first copy. The latest day keeps growing until it ends. Only instances’ traffic is counted: a multi-instance location’s load balancer isn’t included. Egress is not billed.
Deploys
GET /api/v1/applications/{slug}/deploys?limit=20 lists the most recent deploys first. limit goes from 1 to 50.
{
"application": "1234-my-game",
"data": [{
"id": 4321,
"status": "deployed",
"branch": "main",
"commit": "9fceb02",
"message": "Fix matchmaking timeout",
"started_at": "2026-09-24T14:00:03Z",
"finished_at": "2026-09-24T14:02:11Z"
}]
}status is one of:
none: no instance was ready to take the deploy.enqueued: queued, not started yet.deployingdeployedfailed: this includes a deploy superseded by a newer one before it finished.
commit is the short commit hash, as the dashboard shows it. commit and message are null until the deploy has fetched the code. started_at is when the deploy was triggered. finished_at is null until the deploy is deployed or failed.
GET /api/v1/applications/{slug}/deploys/{id} returns one deploy, under data, with one more field: output, the build and deploy output as plain text.
{
"data": {
"id": 4321,
"status": "deployed",
"branch": "main",
"commit": "9fceb02",
"message": "Fix matchmaking timeout",
"started_at": "2026-09-24T14:00:03Z",
"finished_at": "2026-09-24T14:02:11Z",
"output": "[00:01] Installing dependencies...\nadded 246 packages in 7s\n[00:09] Deployed."
}
}output follows a running deploy. It is kept for 30 days, and always for the application’s 10 most recent deploys; past that, output is null. Deploys older than a year are deleted and return a 404.
Billing
GET /api/v1/billing
Your subscriptions and account balance, as on the Billing page.
{
"account": {
"currency": "usd",
"credit_balance": 25,
"balance_due": 0,
"unused_time_credit": 3.5
},
"applications": [{
"id": 1234,
"slug": "1234-my-game",
"name": "My Game",
"subscription": {
"status": "active",
"monthly_price": 18,
"current_period_end": "2026-10-01T00:00:00Z",
"cancel_at_period_end": false
}
}],
"fetched_at": "2026-09-29T15:02:11Z"
}accountis the billing account of your team’s owner, so its balance is shared with any other team they own. It isnullif the owner has no billing account yet.- Amounts are in
account.currencyunits (dollars, not cents).credit_balanceis prepaid credit that pays future invoices;balance_dueis owed and added to the next one.unused_time_creditcomes from downgrades and cancellations, and applies to the next invoice. subscription.statusisactive,past_due,unpaid,incomplete,incomplete_expired,trialingorcanceled, or the subscription isnullif the application has none.monthly_priceis the list price inaccount.currency, before discounts and taxes.
If billing information can’t be fetched, you get a 503 with code billing_unavailable.
Instance logs
GET /api/v1/applications/{slug}/instances/{id}/logs
The latest lines of an instance’s server logs, as text/plain. Errors are still JSON.
| Parameter | Description |
|---|---|
lines | Up to 500. Rounded up to 100 or 500. Default 100. |
stream | all (default), out (stdout only) or err (stderr only). |
The response keeps the newest 256 KB of output, with colour codes stripped.
This endpoint reads the logs from the instance itself, so it allows 2 requests per minute per team, one at a time per instance. For continuous log collection, send your logs from your server code to your logging service instead.
Limits and freshness
Responses are cached for as long as the data stays current, and their Cache-Control header says for how long. Polling faster than this returns the same data.
Every endpoint draws from one budget of 60 requests per minute per team, shared by all of the team’s tokens. Metrics history and instance logs have a tighter limit of their own on top, and still count toward the 60.
| Endpoint | Refreshes every | Requests per minute |
|---|---|---|
/applications | 60 s | 60, shared |
/metrics/current | 30 s | 60, shared |
/metrics | 60 s (minute), 10 min (day) | 10 |
/egress | 10 min | 60, shared |
/deploys | 15 s | 60, shared |
/billing | 10 min | 60, shared |
/instances/{id}/logs | 60 s | 2 |
Rate-limited responses carry X-RateLimit-Limit and X-RateLimit-Remaining, for the tightest limit on that endpoint. Past a limit, you get a 429 with a Retry-After header in seconds. A cached answer still counts toward the limit.
Polling
Between requests, wait for as long as Cache-Control says. After a 429 or a 5xx, wait for as long as Retry-After says. Wait at least a few seconds either way, in case a header is missing or isn’t a number of seconds:
const BASE_URL = "https://cloud-prod.colyseus.io/api/v1";
const headers = { Authorization: `Bearer ${process.env.COLYSEUS_CLOUD_TOKEN}` };
// at least 5 s, and the fallback when the header is missing or a date
const wait = (value, fallback) => Math.max(5, Number(value) || fallback);
async function poll(path, onData) {
while (true) {
const response = await fetch(BASE_URL + path, { headers });
let seconds;
if (response.ok) {
onData(await response.json());
seconds = wait(response.headers.get("cache-control")?.match(/max-age=(\d+)/)?.[1], 60);
} else if (response.status === 429 || response.status >= 500) {
seconds = wait(response.headers.get("retry-after"), 30);
} else {
throw new Error(`${response.status}: ${await response.text()}`);
}
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}
}
poll("/applications/1234-my-game/metrics/current", (metrics) => {
for (const location of metrics.locations) {
console.log(location.region, location.totals?.ccu ?? "no recent data");
}
});Errors
Errors have the same shape and a stable code:
{ "error": { "code": "rate_limited", "message": "Too many requests. Retry after the number of seconds in the Retry-After header." } }| Status | code | Meaning |
|---|---|---|
| 401 | unauthenticated | Missing, invalid or revoked token. |
| 404 | not_found | No such application, deploy or instance in your team. |
| 405 | method_not_allowed | The API only answers GET. |
| 409 | instance_not_ready | The instance isn’t running, so its logs can’t be read. |
| 422 | invalid_request | A parameter is invalid; error.fields maps each one to its messages. |
| 429 | rate_limited, busy | Retry after Retry-After seconds. |
| 500 | server_error | Something failed on our side. |
| 502 | instance_unreachable | The instance didn’t answer the log request. |
| 503 | timeout, busy, unavailable, billing_unavailable | Temporary; retry after Retry-After seconds. |
Under heavy load, the proxy in front of the API can answer a 429, 502 or 504 itself, as plain text rather than JSON. Treat those as temporary too.