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:

Terminal
read -rs COLYSEUS_CLOUD_TOKEN && export COLYSEUS_CLOUD_TOKEN

Find your application’s slug

Terminal
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

Terminal
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.

EndpointReturns
GET /applicationsYour applications, with their regions, plans and instances.
GET /applications/{slug}/metrics/currentEach instance’s latest CCU, rooms, CPU, memory and processes.
GET /applications/{slug}/metricsThe same per location, by minute or by day.
GET /applications/{slug}/egressNetwork traffic per location and per day.
GET /applications/{slug}/deploysRecent deploys, and each one’s output.
GET /billingYour subscriptions and account balance.
GET /applications/{slug}/instances/{id}/logsAn 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

ParameterDescription
resolutionminute (default) or day.
from, toISO 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_idOnly 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.

ParameterDescription
from, toDates (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.
  • deploying
  • deployed
  • failed: 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"
}
  • account is the billing account of your team’s owner, so its balance is shared with any other team they own. It is null if the owner has no billing account yet.
  • Amounts are in account.currency units (dollars, not cents). credit_balance is prepaid credit that pays future invoices; balance_due is owed and added to the next one. unused_time_credit comes from downgrades and cancellations, and applies to the next invoice.
  • subscription.status is active, past_due, unpaid, incomplete, incomplete_expired, trialing or canceled, or the subscription is null if the application has none. monthly_price is the list price in account.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.

ParameterDescription
linesUp to 500. Rounded up to 100 or 500. Default 100.
streamall (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.

EndpointRefreshes everyRequests per minute
/applications60 s60, shared
/metrics/current30 s60, shared
/metrics60 s (minute), 10 min (day)10
/egress10 min60, shared
/deploys15 s60, shared
/billing10 min60, shared
/instances/{id}/logs60 s2

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:

poll.mjs
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." } }
StatuscodeMeaning
401unauthenticatedMissing, invalid or revoked token.
404not_foundNo such application, deploy or instance in your team.
405method_not_allowedThe API only answers GET.
409instance_not_readyThe instance isn’t running, so its logs can’t be read.
422invalid_requestA parameter is invalid; error.fields maps each one to its messages.
429rate_limited, busyRetry after Retry-After seconds.
500server_errorSomething failed on our side.
502instance_unreachableThe instance didn’t answer the log request.
503timeout, busy, unavailable, billing_unavailableTemporary; 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.