Docs

Every route is a GET request to one base URL. There is no key and no SDK: build the URL, use the image.

https://skins.sona.gg
HTML
<img
  src="https://skins.sona.gg/face/jeb_?size=64"
  width="64"
  height="64"
  alt="jeb_"
/>
Markdown
![jeb_](https://skins.sona.gg/face/jeb_?size=64)
Kotlin
import java.net.URI
import javax.imageio.ImageIO

val face = ImageIO.read(URI("https://skins.sona.gg/face/jeb_?size=64").toURL())

Identifiers

The <id> in every path is one of three things. A name that happens to be 32 or 36 characters long is read as a UUID.

Java username
1 to 16 letters, digits or underscores.
jeb_
Java UUID
32 hex digits, with or without dashes.
853c80ef-3c37-49fd-aa49-938b674adae6
Bedrock gamertag
A tilde, then 1 to 16 letters, digits or spaces. Resolved through GeyserMC.
~Steve

Routes

GET /face/<id>

The front of the head, flat: 8 by 8 skin pixels scaled up, hat layer on top.

https://skins.sona.gg/face/jeb_

Takes sizeoverlaybgformatfallback

face render of jeb_

GET /head/<id>

The head in true isometric 3D.

https://skins.sona.gg/head/jeb_

Takes sizeoverlayshadowbgmodelyawpitchformatfallback

head render of jeb_

GET /bust/<id>

Head and torso in 3D, front three-quarter view.

https://skins.sona.gg/bust/jeb_

Takes sizeoverlayshadowbgmodelyawpitchformatfallback

bust render of jeb_

GET /body/<id>

The whole player in 3D, front three-quarter view, standing or in a named pose.

https://skins.sona.gg/body/jeb_

Takes sizeoverlayshadowbgmodelcapeyawpitchposeformatfallback

body render of jeb_

GET /skin/<id>

The raw 64x64 skin PNG. Legacy 64x32 skins are upgraded to 64x64.

https://skins.sona.gg/skin/jeb_

Takes formatfallback

skin texture of jeb_

GET /cape/<id>

The raw cape PNG, or a 404 when the player has no cape.

https://skins.sona.gg/cape/jeb_

Takes format

cape texture of jeb_

Parameters

All are optional query parameters. Unknown ones are ignored; a parameter given twice is an error.

size

Edge length in pixels. Rounded up to the next multiple of 8, so every skin pixel covers a whole number of image pixels.

Value
Integer from 8 to 1024, in steps of 8
Default
256
Routes
/face

Longest edge in pixels. The other edge follows from the view and the pose.

Value
Integer from 8 to 1024
Default
256
Routes
/head/bust/body
https://skins.sona.gg/face/jeb_?size=64
Example of size

overlay

Draw the second skin layer: hat, jacket, sleeves and trousers.

Value
true or false
Default
true
Routes
/face/head/bust/body
https://skins.sona.gg/body/jeb_?overlay=false
Example of overlay

bg

Background colour as 6 or 8 hex digits (rrggbb or rrggbbaa) without the #. Left out, the background is transparent.

Value
Hex colour
Default
none
Routes
/face/head/bust/body
https://skins.sona.gg/bust/jeb_?bg=1f2937
Example of bg

format

Image encoding. Both are lossless.

Value
png, webp
Default
png
Routes
/face/head/bust/body/skin/cape
https://skins.sona.gg/head/jeb_?format=webp
Example of format

fallback

What to send when the player does not exist, in place of the 404: the Steve or Alex default skin, or blank for a fully transparent image.

Value
steve, alex, blank
Default
none
Routes
/face/head/bust/body/skin
https://skins.sona.gg/body/no_such_player_?fallback=steve
Example of fallback

shadow

Soft contact shadow under the feet. Accepted on /head and /bust, which have no ground to cast one on.

Value
true or false
Default
true
Routes
/head/bust/body
https://skins.sona.gg/body/jeb_?shadow=false
Example of shadow

model

Arm width: wide (4 pixels) or slim (3 pixels). Left out, the player's own profile decides.

Value
wide, slim
Default
none
Routes
/head/bust/body
https://skins.sona.gg/body/jeb_?model=slim
Example of model

yaw

Turn around the vertical axis, in degrees. Decimals are allowed.

Value
Number from -180 to 180
Default
45
Routes
/head

Turn around the vertical axis, in degrees. Decimals are allowed.

Value
Number from -180 to 180
Default
30
Routes
/bust/body
https://skins.sona.gg/body/jeb_?yaw=-120
Example of yaw

pitch

Camera elevation in degrees: negative looks up from below, positive looks down from above.

Value
Number from -45 to 45
Default
35.26
Routes
/head

Camera elevation in degrees: negative looks up from below, positive looks down from above.

Value
Number from -45 to 45
Default
15
Routes
/bust/body
https://skins.sona.gg/head/jeb_?pitch=-20
Example of pitch

cape

Draw the player's cape when the profile has one.

Value
true or false
Default
true
Routes
/body
https://skins.sona.gg/body/jeb_?yaw=150&cape=false
Example of cape

pose

The stance of the body.

Value
default, walking, crouching, crossed, relaxing, marching
Default
default
Routes
/body
https://skins.sona.gg/body/jeb_?pose=walking
Example of pose

Responses

A successful request returns the image bytes. The same URL always returns the same bytes until the player changes their skin.

Cache-Control
public, max-age=3600, stale-while-revalidate=86400
On images. Fallback images and 404s use public, max-age=60; other errors are never cached.
ETag
A strong validator of the body. Send it back in If-None-Match to get a 304.
X-Proteus-Cache
HIT when nothing had to be fetched or drawn for this request. MISS when the player's data was fetched or the image was rendered for it. STALE when the answer was built from player data past its freshness window, which happens while a refresh runs in the background and for as long as Mojang cannot be reached.
Access-Control-Allow-Origin
* on every render, texture and profile response, errors included, so images and JSON can be read from any web page. /v1/health is meant for load balancers and sends none.

Errors

Every route above answers its errors as JSON with an error code you can switch on.

invalid_parameter 400

The id or a query parameter is malformed or out of range. The field property names it and the message property says what is allowed.

Body
{
  "error": "invalid_parameter",
  "field": "size",
  "message": "size must be an integer from 8 to 1024"
}

player_not_found 404

No Minecraft account has that username, UUID or gamertag. Add fallback to get a default skin instead.

Body
{
  "error": "player_not_found",
  "id": "no_such_player_"
}

cape_not_found 404

The player exists but has no cape. Only /cape answers with this.

Body
{
  "error": "cape_not_found",
  "id": "Notch"
}

rate_limited 429

One address used up its allowance of 300 requests a minute. A large 3D render that had to be drawn counts as several. Retry-After says how many seconds to wait.

Body
{
  "error": "rate_limited"
}

render_failed 500

The renderer failed on this input. Nothing was cached.

Body
{
  "error": "render_failed"
}

upstream_unavailable 503

Mojang or Geyser did not answer and nothing is cached for this player yet. Retry-After is 30 seconds.

Body
{
  "error": "upstream_unavailable"
}

render_busy 503

Every render slot stayed busy for 2 seconds. Retry-After is 1 second.

Body
{
  "error": "render_busy"
}

not_found 404

No route matches the path. Check the route name and that the id is one path segment.

Body
{
  "error": "not_found"
}

texture_invalid 502

The player exists, but the skin or cape file Mojang serves for them is missing, too large or not a valid texture. Retrying will not help until the player changes it.

Body
{
  "error": "texture_invalid",
  "id": "jeb_"
}

Info routes

GET /v1/profile/<id>

What Proteus resolved for a player: UUID, current username, arm model and texture URLs.

GET /v1/health

Liveness for load balancers, with cache counters, render timings and whether each upstream is reachable.

Rate limits

Each IP address can send a burst of 300 requests, and earns one more every 200 milliseconds, which is 300 a minute. With the allowance spent, requests answer 429 with a Retry-After header in seconds. IPv6 addresses are counted per /64 network. Images served from your browser cache or a CDN do not count, so honouring the cache headers keeps most sites far below the limit.

A head, bust or body larger than 256 pixels counts as more than one request when Proteus has to draw it: divide the size by 256, round up and square it, so 512 counts as 4 and 1024 as 16. The same image answered from the cache counts as one.

Sona and Clover services send an X-Proteus-Key header and are not limited. Keys are not self-serve; ask in the Sona Discord if your project needs one.

Proteus is run by Sona Interactive for Clover Client and anyone else who needs skin renders. It is not affiliated with, endorsed by or connected to Mojang or Microsoft. Minecraft is a trademark of Mojang.