Exylia Skins API

API v1Base URL https://skins-api.exylia.net. No key, no account. JSON in, JSON out.

What it does

A Minecraft client only draws a head or a player with a texture hosted on textures.minecraft.net, and the only way a picture gets there is a Java account wearing it. This API keeps a pool of such accounts and a global cache: send a 64x64 skin PNG, get its Mojang texture. Every image is uploaded once, ever, for everybody.

Quick start

curl
curl -X POST https://skins-api.exylia.net/v1/textures \
  -H "Content-Type: application/json" \
  -H "User-Agent: MyPlugin/1.0" \
  -d "{\"images\":[{\"png\":\"$(base64 -w0 skin.png)\"}]}"
Java 11+
HttpClient http = HttpClient.newHttpClient();
String png = Base64.getEncoder().encodeToString(Files.readAllBytes(Path.of("skin.png")));

HttpRequest request = HttpRequest.newBuilder(URI.create("https://skins-api.exylia.net/v1/textures"))
    .header("Content-Type", "application/json")
    .header("User-Agent", "MyPlugin/1.0")
    .POST(HttpRequest.BodyPublishers.ofString("{\"images\":[{\"png\":\"" + png + "\"}]}"))
    .build();

String body = http.send(request, HttpResponse.BodyHandlers.ofString()).body();
// {"textures":[{"hash":"9f2c…","status":"ready","id":"3b1a…"}]}
// "queued"? Poll GET /v1/textures?hashes=9f2c… after "retryAfter" seconds.

Hash

The key of every texture. Both sides compute it the same way, so a client can ask by hash without sending the picture.

  1. Decode the PNG to 64x64 RGBA (8 bits per channel, row-major, top-left first). 64x32 legacy skins are refused: convert them first.
  2. hash = lowercase hex SHA-256 of those 16384 bytes.

Transparent pixels keep their colour: Minecraft draws a skin's base layer opaque, so it shows. Two PNGs with the same pixels in a different encoding are one texture.

Texture

{
  "hash": "9f2c…",
  "variant": "classic",
  "status": "ready",
  "id": "3b1a…",
  "value": "ewog…",
  "signature": "Kd9…",
  "position": 12
}
FieldWhenMeaning
statusalwaysready, queued, uploading, failed, or unknown (only from GET: never sent)
idreadyThe end of http://textures.minecraft.net/texture/<id>. Enough for heads.
value, signatureready, when an account of the pool uploaded itSigned textures property, for NPCs and player profiles.
positionqueuedUploads ahead of it in the queue.
errorfailedWhy. Sending it again requeues it.

A texture is ready without signature when its Mojang hash already existed on Mojang (it cost nothing). Send it with "signed": true to have an account wear it anyway.

POST /v1/textures

Asks for up to 32 images at once (one ragdoll skin, all its pieces). Ready ones answer at once; the rest are queued. Idempotent: sending the same images again queues nothing new.

request
{
  "images": [{ "png": "<base64>", "variant": "classic" }],
  "signed": false,
  "skin": "3b1a…",
  "quality": "normal"
}
  • variant: classic (default) or slim. Only matters for whole skins; ragdoll pieces send classic.
  • skin, quality (optional): the Mojang texture id of the skin the images were cut from and the ExyliaLib quality (high, normal, low). Recorded so the public player search can show a skin's pieces.

Answer 200, textures in the order of images. retryAfter (seconds) is present while any is queued or uploading: poll with GET no sooner than that.

200
{
  "textures": [
    { "hash": "9f2c…", "status": "ready", "id": "3b1a…" },
    { "hash": "77d0…", "status": "queued", "position": 3 }
  ],
  "retryAfter": 20
}

GET /v1/textures?hashes=h1,h2,…&variant=classic

Up to 64 hashes. Never uploads and never counts against the upload cap, so a client asks here first and POSTs only what is unknown or failed.

Answer 200: { "textures": [...], "retryAfter": 20 }, in the order asked.

GET /v1/textures/{hash}?variant=classic

One texture, or 404 when unknown.

GET /v1/stats

Public totals: { "textures", "uploads", "existing", "hits", "queued", "accounts", "throughputPerHour" }.

Limits and errors

StatusWhen
400Not a PNG, not 64x64, larger than 16 KiB, more than 32 images or 64 hashes. Body { "error": "…" }.
429More than 120 requests a minute from one IP, or more than 400 new uploads queued from one IP in an hour. Retry-After header in seconds.
503Database down. Retry later.

Images over the per-IP cap are not refused one by one: the whole request is a 429, so a client keeps its queue and retries after Retry-After.

Send a User-Agent naming your software (ExyliaLib/2.4.0).

Fairness

The queue is served round-robin by requester (IP), oldest first within each, so one busy network cannot starve a small server.

More