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 -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)\"}]}"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.
- Decode the PNG to 64x64 RGBA (8 bits per channel, row-major, top-left first). 64x32 legacy skins are refused: convert them first.
- 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
}| Field | When | Meaning |
|---|---|---|
| status | always | ready, queued, uploading, failed, or unknown (only from GET: never sent) |
| id | ready | The end of http://textures.minecraft.net/texture/<id>. Enough for heads. |
| value, signature | ready, when an account of the pool uploaded it | Signed textures property, for NPCs and player profiles. |
| position | queued | Uploads ahead of it in the queue. |
| error | failed | Why. 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.
{
"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.
{
"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
| Status | When |
|---|---|
| 400 | Not a PNG, not 64x64, larger than 16 KiB, more than 32 images or 64 hashes. Body { "error": "…" }. |
| 429 | More 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. |
| 503 | Database 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.