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 <img
src="https://skins.sona.gg/face/jeb_?size=64"
width="64"
height="64"
alt="jeb_"
/>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_ GET /head/<id>
The head in true isometric 3D.
https://skins.sona.gg/head/jeb_ GET /bust/<id>
Head and torso in 3D, front three-quarter view.
https://skins.sona.gg/bust/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
GET /skin/<id>
The raw 64x64 skin PNG. Legacy 64x32 skins are upgraded to 64x64.
https://skins.sona.gg/skin/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
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 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 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 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 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 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 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 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 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 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 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 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.
{
"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.
{
"error": "player_not_found",
"id": "no_such_player_"
}cape_not_found 404
The player exists but has no cape. Only /cape answers with this.
{
"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.
{
"error": "rate_limited"
}render_failed 500
The renderer failed on this input. Nothing was cached.
{
"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.
{
"error": "upstream_unavailable"
}render_busy 503
Every render slot stayed busy for 2 seconds. Retry-After is 1 second.
{
"error": "render_busy"
}not_found 404
No route matches the path. Check the route name and that the id is one path segment.
{
"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.
{
"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.