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 thefingerprintkey 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:
https://www.ecstacy.ac/api/v1For 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.
curl -H "X-API-Key: $ECSTACY_API_KEY" \
https://www.ecstacy.ac/api/v1/infoWarning
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 |
curl -H "X-API-Key: $ECSTACY_API_KEY" https://www.ecstacy.ac/api/v1/servers{
"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.
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:
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:
{ "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:
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 anywayThe same shape in shell, for a health check or a cron job:
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.