HTTP API

Plan requirement. Access is included with the SMP and Network plans. A key on another plan gets 403 {"error":"api_not_available_on_plan"}. The detected-client fingerprint is a separate capability on the same plans: without it the fingerprint key is absent rather than empty. The brand a client reports at join is not gated.

A read-only JSON API for reading licence, server and player data from outside Minecraft. Base URL:

TEXT
https://www.ecstacy.ac/api/v1

For anything running inside the server, use the plugin API instead. It is synchronous, has no rate limit, and does not go over the network.

Authentication

Every request needs your API key in a header. The key is issued with your licence and is visible in the Client Area.

BASH
curl -H "X-API-Key: $ECSTACY_API_KEY" \
     https://www.ecstacy.ac/api/v1/info

Warning

The key scopes every response to your licence. Treat it like a password: server-side only, never in a client, never in a public repository. If it leaks, rotate it from the Client Area.

Licence-scoped endpoints

GET /api/v1/{info} where {info} is one of:

{info} Returns
info or license Licence id, masked key suffixes, plan limits (maxPlayers, maxInstances, maxInstancesPerIp, maxIps, allowedUuids, allowedIps), activeInstances
servers count and every connected instance: uuid, name, ip
players count and every tracked player: uuid, name, serverId
violations count: total violations across the licence
BASH
curl -H "X-API-Key: $ECSTACY_API_KEY" https://www.ecstacy.ac/api/v1/servers
JSON
{
  "count": 2,
  "servers": [
    { "uuid": "…", "name": "Practice-EU", "ip": "…" }
  ]
}

Server-scoped endpoints

GET /api/v1/{serverUuid}/{info} where {serverUuid} is a connected instance from /servers:

{info} Returns
info or server uuid, name, ip, join
players count and each player's uuid, name, join
violations count for that server only

Player endpoints

Here the {uuid} in the path is the player's UUID, not a server's.

Endpoint Returns
GET /api/v1/{uuid}/player/info uuid, name, reputation, and fingerprint when one is known and the plan includes it
GET /api/v1/{uuid}/player/reputation globalRank: TRUSTED, WATCH or HIGH_RISK
GET /api/v1/{uuid}/player/violations Array of violations: check_name, check_type, timestamp, vl, maxVl
GET /api/v1/{uuid}/player/logs Array of audit-log entries

violations and logs accept ?limit=: 1 to 200, defaulting to 20.

BASH
curl -H "X-API-Key: $ECSTACY_API_KEY" \
     "https://www.ecstacy.ac/api/v1/069a79f4-44e9-4726-a5be-fca90e38aaf5/player/violations?limit=50"

Rate limits

Limit Value
Per IP 60 requests/minute
Per API key Reported in the response headers
Invalid-key lockout 10 bad keys from one IP triggers a temporary block

Every response carries the current state:

TEXT
X-RateLimit-Limit:     <per-key limit>
X-RateLimit-Remaining: <requests left this window>
X-RateLimit-Reset:     <unix seconds when the window resets>

Exceeding a limit returns 429 with Retry-After: 60.

Tip

Responses are cached server-side for 30 seconds, so polling faster than that returns the same body while still consuming your quota. Poll on a 30-second floor, or longer.

Errors

Status Meaning
404 Invalid API key, or the requested server UUID does not belong to this licence
429 Rate limit exceeded, or too many invalid-key attempts from your IP
500 Server-side failure. The body carries an error field

Errors are JSON with an error string:

JSON
{ "error": "Invalid api key" }

An unrecognised {info} value returns 200 with an error field rather than a 4xx, so check for error in the body as well as the status code.

Note that a wrong API key returns 404, not 401 or 403: the exception is the plan gate, which is an explicit 403 {"error":"api_not_available_on_plan"}. If you are getting 404 on every endpoint, check the key before you check the path.

A worked example

Listing the servers on a licence and then the violations for each, with the two things people usually get wrong. Checking error in the body, and respecting the 30-second cache:

poll.py
import os, time, requests

BASE = "https://www.ecstacy.ac/api/v1"
HEAD = {"X-API-Key": os.environ["ECSTACY_API_KEY"]}

def get(path):
    r = requests.get(f"{BASE}/{path}", headers=HEAD, timeout=10)
    if r.status_code == 429:
        time.sleep(int(r.headers.get("Retry-After", 60)))
        return get(path)
    r.raise_for_status()
    body = r.json()
    if "error" in body:                 # 200 with an error field is a real case
        raise RuntimeError(body["error"])
    return body

servers = get("servers")["servers"]
for s in servers:
    total = get(f"{s['uuid']}/violations")["count"]
    print(f"{s['name']}: {total} violations")

time.sleep(30)                          # responses are cached for 30s anyway

The same shape in shell, for a health check or a cron job:

BASH
curl -s -H "X-API-Key: $ECSTACY_API_KEY" \
     https://www.ecstacy.ac/api/v1/info \
  | jq -e '.activeInstances, .maxInstances'

Choosing between this and the plugin API

They read the same data and are not interchangeable.

HTTP API Plugin API
Runs Anywhere Inside the Minecraft server
Latency Network round trip, 30s cache In-process
Rate limited Yes No
Plan gated SMP and Network only No
Live events No. Polling only Yes, via events
Right for Discord bots, web panels, dashboards, monitoring Anything reacting to a flag as it happens

If you are writing a plugin, use the plugin API. Reaching back out over HTTP from inside the server you are already running in adds a network hop, a rate limit and a 30-second delay to data you could have had synchronously.

Integration notes

Discord bots and panels. Poll /{serverUuid}/violations on a 60-second timer rather than per message, and cache the /servers list. Instance UUIDs do not change while a server is connected. Sixty requests a minute per IP disappears quickly if every panel page load makes its own call.

Monitoring. /info is the cheapest liveness endpoint and returns activeInstances, which is what you actually want to alarm on: instances dropping below expectation means a server lost its cloud link, which means it is not detecting anything.

Player lookups. /{uuid}/player/info takes a player UUID, while /{serverUuid}/info takes a server UUID, and both live at the same position in the path. Passing the wrong one returns 404 rather than a helpful message, and it is the single most common integration bug on this API.

Absent versus empty. When the plan does not include the client fingerprint, the fingerprint key is absent from the response rather than present and null. Code that reads it with a subscript will raise; use a defaulted lookup.

Important

This API is read-only. There is no endpoint that bans, exempts, resets reputation or edits configuration, and there will not be one. Those actions run through the in-game commands and the Client Area, where they are attributable to a person.

Last updated