{
  "openapi": "3.1.0",
  "info": {
    "title": "AlloyMC API",
    "version": "1.0.0",
    "summary": "Profiles, follows, linked Minecraft accounts and Alloy releases.",
    "description": "The API behind AlloyMC accounts and the Alloy launcher. Public reads need no key. Writes use a bearer token from signing in through Alloy.",
    "contact": {
      "name": "AlloyMC developers",
      "url": "https://api.alloymc.dev/docs",
      "email": "contact@elchi.dev"
    },
    "termsOfService": "https://legal.alloymc.dev/terms"
  },
  "servers": [
    {
      "url": "https://api.alloymc.dev"
    }
  ],
  "tags": [
    {
      "name": "Status",
      "description": "Is it up, and what's the newest Alloy."
    },
    {
      "name": "Users",
      "description": "Public profiles and who follows whom."
    },
    {
      "name": "Me",
      "description": "The signed-in account. Needs a bearer token."
    },
    {
      "name": "Minecraft",
      "description": "Link a Minecraft account with Mojang's own proof, and its skin renders."
    },
    {
      "name": "Auth",
      "description": "How Alloy turns a browser sign-in into a token."
    }
  ],
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "An app token from POST /v1/auth/token. Sent as Authorization: Bearer <token>."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "example": "handle_taken"
              },
              "message": {
                "type": "string",
                "example": "That handle is taken."
              }
            }
          }
        }
      },
      "MinecraftAccount": {
        "type": "object",
        "properties": {
          "uuid": {
            "type": "string",
            "example": "069a79f444e94726a5befca90e38aaf5"
          },
          "name": {
            "type": "string",
            "example": "Notch"
          },
          "slim": {
            "type": "boolean"
          },
          "headUrl": {
            "type": "string",
            "format": "uri"
          },
          "bodyUrl": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "User": {
        "type": "object",
        "properties": {
          "handle": {
            "type": "string",
            "example": "elchi"
          },
          "displayName": {
            "type": "string",
            "example": "Elchi"
          },
          "bio": {
            "type": "string"
          },
          "pronouns": {
            "type": "string"
          },
          "links": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "accent": {
            "type": "string",
            "enum": [
              "gold",
              "copper",
              "redstone",
              "emerald",
              "diamond",
              "lapis",
              "amethyst",
              "steel"
            ]
          },
          "private": {
            "type": "boolean"
          },
          "avatarUrl": {
            "type": "string",
            "format": "uri"
          },
          "profileUrl": {
            "type": "string",
            "format": "uri"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "followers": {
            "type": "integer"
          },
          "following": {
            "type": "integer"
          },
          "minecraft": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MinecraftAccount"
            }
          }
        }
      },
      "Me": {
        "allOf": [
          {
            "$ref": "#/components/schemas/User"
          },
          {
            "type": "object",
            "properties": {
              "email": {
                "type": "string"
              },
              "emailVerified": {
                "type": "boolean"
              },
              "hasPassword": {
                "type": "boolean"
              },
              "hasGoogle": {
                "type": "boolean"
              },
              "avatar": {
                "type": "string",
                "example": "minecraft:069a79f444e94726a5befca90e38aaf5"
              }
            }
          }
        ]
      },
      "Session": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "web",
              "app"
            ]
          },
          "device": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastUsed": {
            "type": "string",
            "format": "date-time"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "current": {
            "type": "boolean"
          }
        }
      },
      "Brief": {
        "type": "object",
        "properties": {
          "handle": {
            "type": "string"
          },
          "displayName": {
            "type": "string"
          },
          "accent": {
            "type": "string"
          },
          "avatarUrl": {
            "type": "string",
            "format": "uri"
          }
        }
      }
    }
  },
  "paths": {
    "/v1/status": {
      "get": {
        "tags": [
          "Status"
        ],
        "summary": "Server status",
        "operationId": "status",
        "responses": {
          "200": {
            "description": "Up",
            "content": {
              "application/json": {
                "example": {
                  "ok": true,
                  "version": "0.1.9",
                  "launcher": "0.2.1"
                }
              }
            }
          }
        }
      }
    },
    "/v1/launcher/releases/latest": {
      "get": {
        "tags": [
          "Status"
        ],
        "summary": "Newest Alloy release and its signed update files",
        "operationId": "latestRelease",
        "responses": {
          "200": {
            "description": "The newest release",
            "content": {
              "application/json": {
                "example": {
                  "version": "0.2.1",
                  "published": "2026-10-30T18:15:43Z",
                  "updates": {
                    "windows-x86_64": {
                      "url": "https://github.com/…",
                      "size": 5666631,
                      "sha256": "…"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/users/{handle}": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "A public profile",
        "operationId": "getUser",
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "elchi"
          }
        ],
        "responses": {
          "200": {
            "description": "The profile. Private profiles return only handle, displayName, avatarUrl and private: true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "301": {
            "description": "The handle changed recently; Location points at the new one."
          },
          "404": {
            "description": "No such handle",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/users/{handle}/followers": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Who follows someone, newest first",
        "operationId": "followers",
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Cursor from the previous page's next."
          }
        ],
        "responses": {
          "200": {
            "description": "Up to 50 people and a cursor",
            "content": {
              "application/json": {
                "example": {
                  "users": [
                    {
                      "handle": "someone",
                      "displayName": "Someone",
                      "accent": "lapis",
                      "avatarUrl": "https://api.alloymc.dev/v1/users/someone/avatar.png"
                    }
                  ],
                  "next": 0
                }
              }
            }
          }
        }
      }
    },
    "/v1/users/{handle}/following": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Who someone follows, newest first",
        "operationId": "following",
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Up to 50 people and a cursor"
          }
        }
      }
    },
    "/v1/users/{handle}/follow": {
      "put": {
        "tags": [
          "Users"
        ],
        "summary": "Follow someone",
        "operationId": "follow",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Following (also if you already were)"
          }
        }
      },
      "delete": {
        "tags": [
          "Users"
        ],
        "summary": "Unfollow someone",
        "operationId": "unfollow",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Not following"
          }
        }
      }
    },
    "/v1/users/{handle}/avatar.png": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Avatar image",
        "operationId": "avatar",
        "description": "The linked Minecraft head or the person's sigil, as a crisp pixel PNG.",
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "s",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 8,
              "maximum": 512,
              "default": 128
            },
            "description": "Size in pixels, rounded down to a multiple of 8."
          }
        ],
        "responses": {
          "200": {
            "description": "PNG",
            "content": {
              "image/png": {}
            }
          }
        }
      }
    },
    "/v1/me": {
      "get": {
        "tags": [
          "Me"
        ],
        "summary": "Your account",
        "operationId": "me",
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "You, including your email",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "description": "Missing or expired token"
          }
        }
      },
      "patch": {
        "tags": [
          "Me"
        ],
        "summary": "Change your profile",
        "operationId": "updateMe",
        "security": [
          {
            "bearer": []
          }
        ],
        "description": "Send only the fields to change: handle, displayName, bio, pronouns, links, accent, avatar (\"sigil\" or \"minecraft:<uuid>\"), private.",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "bio": "Builds castles. Breaks modpacks.",
                "accent": "amethyst"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "400": {
            "description": "Not valid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/sessions": {
      "get": {
        "tags": [
          "Me"
        ],
        "summary": "Where you're signed in",
        "operationId": "sessions",
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Sessions, most recently used first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sessions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Session"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/sessions/{id}": {
      "delete": {
        "tags": [
          "Me"
        ],
        "summary": "Sign one session out",
        "operationId": "revokeSession",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Signed out"
          }
        }
      }
    },
    "/v1/me/minecraft/challenge": {
      "post": {
        "tags": [
          "Minecraft"
        ],
        "summary": "Start linking a Minecraft account",
        "operationId": "minecraftChallenge",
        "security": [
          {
            "bearer": []
          }
        ],
        "description": "Returns a server ID valid for two minutes. Pass it to Mojang's POST https://sessionserver.mojang.com/session/minecraft/join with the player's own access token, exactly as when joining a server; then call POST /v1/me/minecraft.",
        "responses": {
          "200": {
            "description": "A server ID",
            "content": {
              "application/json": {
                "example": {
                  "serverId": "alloymc1f3a9c0d2e7b4",
                  "expiresIn": 120
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/minecraft": {
      "post": {
        "tags": [
          "Minecraft"
        ],
        "summary": "Finish linking",
        "operationId": "linkMinecraft",
        "security": [
          {
            "bearer": []
          }
        ],
        "description": "AlloyMC asks Mojang's hasJoined who joined with that server ID. The Minecraft token never reaches AlloyMC.",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "username": "Notch",
                "serverId": "alloymc1f3a9c0d2e7b4"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Linked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MinecraftAccount"
                }
              }
            }
          },
          "403": {
            "description": "Mojang couldn't confirm the join"
          },
          "409": {
            "description": "Linked to another AlloyMC account"
          }
        }
      }
    },
    "/v1/me/minecraft/{uuid}": {
      "delete": {
        "tags": [
          "Minecraft"
        ],
        "summary": "Unlink a Minecraft account",
        "operationId": "unlinkMinecraft",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "uuid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Unlinked"
          }
        }
      }
    },
    "/v1/minecraft/{uuid}/head.png": {
      "get": {
        "tags": [
          "Minecraft"
        ],
        "summary": "Head render of a linked account",
        "operationId": "minecraftHead",
        "parameters": [
          {
            "name": "uuid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "s",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 64
            }
          }
        ],
        "responses": {
          "200": {
            "description": "PNG with the hat layer",
            "content": {
              "image/png": {}
            }
          }
        }
      }
    },
    "/v1/minecraft/{uuid}/body.png": {
      "get": {
        "tags": [
          "Minecraft"
        ],
        "summary": "Full-body front render of a linked account",
        "operationId": "minecraftBody",
        "parameters": [
          {
            "name": "uuid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "s",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 128
            },
            "description": "Width in pixels; the height is twice that."
          }
        ],
        "responses": {
          "200": {
            "description": "PNG",
            "content": {
              "image/png": {}
            }
          }
        }
      }
    },
    "/v1/auth/token": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Trade a sign-in code for a token",
        "operationId": "token",
        "description": "Alloy opens https://alloymc.dev/auth/app?challenge=…&state=…&device=… in the browser (PKCE S256). After the person allows it, the browser returns to alloy://auth?code=…&state=…; Alloy posts the code and its verifier here. Codes last five minutes and work once.",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "code": "…",
                "verifier": "…"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A token, valid 90 days and renewed while used",
            "content": {
              "application/json": {
                "example": {
                  "token": "…",
                  "tokenType": "Bearer",
                  "user": {
                    "handle": "elchi"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown, used or expired code, or wrong verifier"
          }
        }
      },
      "delete": {
        "tags": [
          "Auth"
        ],
        "summary": "Sign this token out",
        "operationId": "signOut",
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "204": {
            "description": "The token no longer works"
          }
        }
      }
    },
    "/v1/minecraft/{uuid}": {
      "get": {
        "tags": [
          "Minecraft"
        ],
        "operationId": "getPlayer",
        "summary": "Find the AlloyMC profile behind a player",
        "description": "Looks up a Minecraft account by UUID (with or without dashes). Answers only when the player is linked to a public AlloyMC profile and shown on it; otherwise 404, so hidden links stay hidden.",
        "parameters": [
          {
            "name": "uuid",
            "in": "path",
            "required": true,
            "description": "Minecraft UUID, with or without dashes",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The player and their AlloyMC profile",
            "content": {
              "application/json": {
                "example": {
                  "uuid": "069a79f444e94726a5befca90e38aaf5",
                  "name": "Notch",
                  "slim": false,
                  "headUrl": "https://api.alloymc.dev/v1/minecraft/069a79f444e94726a5befca90e38aaf5/head.png",
                  "bodyUrl": "https://api.alloymc.dev/v1/minecraft/069a79f444e94726a5befca90e38aaf5/body.png",
                  "user": {
                    "handle": "kestrel",
                    "displayName": "Kestrel",
                    "accent": "amethyst",
                    "avatarUrl": "https://api.alloymc.dev/v1/users/kestrel/avatar.png?v=1a2b3c4d",
                    "profileUrl": "https://alloymc.dev/@kestrel"
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not linked to a public profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "AlloyMC developer docs",
    "url": "https://api.alloymc.dev/docs"
  }
}