Aphanite General API
The part shared between Yggdrasil and Phenocryst in the Aphanite system is called the General API.
This page defines the endpoints and implementation details of the Aphanite General API, using TypeScript types to describe the expected request bodies and response payloads.
Basic Conventions
Unless otherwise specified, both the Aphanite General API and Phenocryst API follow these conventions:
- Request and response bodies must be in JSON format, with the correct
Content-Type: application/jsonheader. - The server responds with the following format regardless of success:typescriptIf an error occurs, the HTTP status code should be set appropriately, but the
type Response<Payload> = { success: boolean; // Whether the operation succeeded payload?: Payload; // The actual response data on success reason?: string; // A human-readable error reason on failure }; // Or more specifically: type Response<Payload> = { success: true; payload: Payload; } | { success: false; reason: string; };reasonfield should contain the actual cause of the error — it doesn't have to match the HTTP Reason Phrase. Thepayloadtype is unrestricted and can be any JSON-representable type (determined by the specific business API), but cannot be empty. If there's nothing meaningful to return, use204 No Content. All response body types mentioned below are treated as thePayloadgeneric parameter here. - All endpoint paths mentioned below are subdirectories of
<aphanite_base_url>/api.
Data Models
Common data models referenced in the responses below.
// User metadata
type User = {
id: string; // User UUID
name: string; // User name (note: not unique)
email: string; // User email
permissions: Permission[]; // User permissions
};
// User permissions. Stored as numeric bit flags internally,
// but serialized as an enum array. Clients just need to parse the enum.
const enum Permission {
Management = "management",
}
// Player profile metadata
type Profile = {
id: string; // Profile UUID
name: string; // In-game name. **ASCII strings only**
owner: string; // UUID of the Aphanite user who owns this profile
};
// Player skin data
type ProfileSkin = {
skin?: string; // Skin URL
model?: "default" | "slim"; // Arm thickness
cape?: string; // Cape URL
}Authentication
Aphanite uses a token-based authentication mechanism. To avoid the complexity of maintaining multiple systems, token issuance shares the same pool as Yggdrasil. That is, Phenocryst clients only need to log in once to access both Phenocryst API and Yggdrasil API.
However, although tokens share the same pool as Minecraft authentication, Aphanite has its own user model. Clients designed for Aphanite should use Aphanite's own API to obtain Aphanite-specific user information.
Endpoints marked with Auth required require authentication; endpoints marked with Auth required (Admin) require authentication and the current user must have Management permission.
Login
Authenticate with email and password to get user info and issue a set of auth tokens.
POST /auth/loginRequest body:
type Request = {
email: string; // Account email
password?: string; // Account password (plain text)
otp_token?: string; // OTP challenge result
}The otp_token value and how to obtain it are described in OTP Verification.
The purpose of OTP here is to prevent credential-stuffing attacks in case Aphanite's password database is leaked. However, unlike traditional security practices, OTP in Aphanite acts as 1FA — it carries the same authority as a password. This does make Aphanite somewhat less secure.
Clients should prefer using OTP to obtain tokens, treating passwords as a secondary, fallback option.
Response:
type Payload = {
access_token: string;
client_token: string;
user: User;
}Note: Client Token is only used in special Yggdrasil API scenarios. We agree to ignore it in Aphanite General and Phenocryst APIs. However, launchers should still store it for when it's needed.
Refresh Token Auth required
Tokens expire after 15 days and can be used for authentication within that period. In principle, Phanerite should refresh tokens once per day. Since the Yggdrasil token refresh API involves profile selection and such, we provide a simpler API.
POST /auth/refreshRequest body is empty.
Response:
type Payload = {
access_token: string; // New Access Token
user: User; // The user info associated with the token; launchers should update their local storage — useful if the user has changed their info
}The Client Token remains unchanged after refreshing; the server no longer returns it.
Validate Token Auth required
GET /auth/validateReturns 204 No Content if the token is valid.
API Authorization
In Phenocryst and other General APIs, authentication is done by putting the Access Token as a Bearer token in the Authorization header, e.g.:
POST /api/endpoint HTTP/1.1
Authorization: Bearer access_token
Content-Type: application/json; charset=utf-8
Content-Length: 18
{"request":"body"}Requests to Auth required endpoints without authentication info, or with invalid auth (e.g., expired token), return 401 Unauthorized.
OTP Verification
Verication Flow
The OTP mechanism is designed for passwordless login or password resets, so it does not require authentication.
Verification uses a session-based flow:
- The client requests OTP session creation.
- The client receives the session ID and waits for the verification code. (In design, codes can be admin panel codes, email codes, or TOTP; currently only TOTP is supported.)
- The client uses the session ID and code to complete OTP verification.
- The client receives an
otp_token, a one-time token with the same authority as a password, usable with the Login API.
TOTP
Phanerite should store the TOTP secret locally instead of storing the plain-text password. This allows persistent login without frequent password entry, without storing the user's actual password.
To create a TOTP private key, the client should request Issue or rotate TOTP private key while logged in.
To log in with TOTP, the client follows the verification flow above, sets method to totp, and submits the current TOTP code as the OTP verification code.
TOTP uses the algorithm defined in RFC 6238, with a 30-second time step and 6-digit length.
Create OTP Session
POST /verificationRequest body:
type Request = {
email: string;
method: string;
}As of Aphanite v0.1.0, method only supports totp.
Response:
type Payload = {
id: string;
}- Returns
400 Bad Requestif the user has no activated TOTP private key.
Complete OTP Verification
POST /verification/{id}Request body:
type Request = {
code: string; // OTP verification code
}Response:
type Payload = {
otp_token: string;
}- Returns
404 Not Foundif the OTP session ID doesn't exist. - Returns
401 Unauthorizedif the session exists but the code is wrong.
User System
Most APIs below provide two endpoints:
- One under
/users/{id}, targeting a specific user by UUID. - One under
/users/me, representing the current authenticated user.
Get User Info Auth required
GET /users/{id}
GET /users/meResponse:
type Payload = User;Permission check:
- Requesting
/user/me— succeeds. - Requesting
/users/{id}whereidmatches the current user — succeeds. - Requesting
/users/{id}whereiddiffers — checks if the current user has Management permission. - Returns
401 Unauthorizedif no auth or invalid auth.
Update User Metadata Auth required
PATCH /users/{id}
PATCH /users/meRequest body:
type Request = {
name?: string;
email?: string;
}Response:
type Payload = User;Permission check: Same as Get User Info.
Note: Aphanite user email addresses are globally unique. Returns 409 Conflict if the target email is already associated with another user.
Change Password Partially requires auth
PATCH /users/{id}/credentials/password
PATCH /users/me/credentials/passwordRequest body:
type Request = {
old_password?: string;
otp_token?: string;
new_password: string;
}Returns 204 No Content on success.
Permission check:
- Without valid auth:
- Cannot request the ID-less endpoint.
- Must provide
otp_tokenorold_password.
- With valid auth:
- May omit
otp_tokenandold_password. - Other restrictions same as Get User Info.
- May omit
The otp_token semantics and how to obtain it are described in OTP Verification.
Issue or Rotate TOTP Private Key Auth required
Note: Given the practical situation, we don't expect users to manage TOTP themselves — that's Phanerite's job.
POST /users/me/credentials/totpRequest body is empty.
Response:
type Payload = {
secret: string; // TOTP secret key
otpauth_url: string; // Secret key URL in otpauth:// format
}The TOTP is temporary at this point. The user must complete a TOTP challenge to confirm successful setup (see Activate TOTP).
After a successful request, the old TOTP key is immediately invalidated. Phanerite should store the new key.
Note: This endpoint has no user-ID-parameterized version; it only operates on the currently logged-in user.
Activate TOTP Auth required
A newly issued or rotated TOTP key is considered inactive and cannot be used for authentication. The client must perform the following operation once to activate it.
PATCH /users/me/credentials/totpRequest body:
type Request = {
otp_token: string;
}Returns 204 No Content on success.
Disable TOTP Auth required
DELETE /users/me/credentials/totpReturns 204 No Content on success.
Create User (Admin) Auth required (Admin)
This endpoint is designed for administrators to create users from the admin panel. General registration should not use this endpoint (see Registration).
POST /userRequest body:
type Request = {
email: string;
name?: string; // Uses email if not specified
permissions: Permission[];
}Response:
type Payload = User & { password: string };Only admin users can call this endpoint; otherwise returns 403 Forbidden.
Users created this way get a randomly generated password. They should change it immediately after first login.
Registration
This is a relatively complex feature, so it gets its own section.
For Public Instances
This covers the case where config.service.public is true — the server owner expects their Aphanite instance to be a general-purpose, public skin server allowing open registration.
In this case, we introduce Cloudflare Turnstile to prevent abuse (i.e., bulk bot registration). Turnstile can be disabled — though we strongly advise against it.
Get Turnstile Site Key
For displaying the Turnstile widget client-side, see the Turnstile documentation.
GET /turnstileResponse:
type Payload={
site_key:string;
}- Returns
403 Forbiddenif the server hasn't enabled public registration. - Returns
404 Not Foundif the server has public registration but Turnstile is not enabled.
For Private Instances
User creation for private instances is more involved — it requires administrator approval. An admin creates a temporary registration token (valid only for the registration endpoint), which the user can then use to request registration.
TIP
Suggested frontend implementation: Append the token as a query parameter to the registration URL. Frontend JS automatically extracts it and uses it for authentication — achieving registration via a special URL.
Create Registration Token Auth required (Admin)
POST /register/sessionRequest body:
type Payload = {
expires_after: number; // Registration token lifetime in minutes, max 10080 minutes (7 days)
}Response:
type Payload={
token: string;
};Register
Both private and public instances use the same /register endpoint. However, depending on the registration type and whether Turnstile is enabled, turnstile_token and register_token are optional.
POST /registerRequest body:
type Request={
register_token?:string;
turnstile_token?:string;
email:string;
name?:string; // Defaults to email if not specified. Should not exceed 20 characters.
password:string; // Should be more than 8 characters and less than 128 characters.
}Response:
type Payload=User;- If the server has public registration enabled:
- If Turnstile is enabled:
- If neither a registration token nor Turnstile token is provided →
400 Bad Request. - If a Turnstile token is provided but fails validation →
422 Unprocessable Content. - If a registration token is provided but fails validation →
403 Forbidden.
- If neither a registration token nor Turnstile token is provided →
- If Turnstile is not enabled:
- No auth-related failures.
- If Turnstile is enabled:
- If the server does not have public registration:
- If no registration token is provided (Turnstile token ignored) →
400 Bad Request. - If a registration token is provided but fails validation →
403 Forbidden.
- If no registration token is provided (Turnstile token ignored) →
- If the email conflicts with an existing user →
409 Conflict. - If the name and password failed the length checks →
418 I'm a Teapot.
Profiles
A Profile represents a Minecraft player entity — containing the player name, UUID, textures, etc.
List User Profiles Auth required
List all player profiles for the current user or a specified user.
GET /users/{id}/profiles?with_skin={bool}
GET /users/me/profiles?with_skin={bool}Response:
type Payload = Array<{
metadata: Profile;
skin?: ProfileSkin;
}>;- If the auth info is invalid →
401 Unauthorized. - If requesting the endpoint without an ID:
- Succeeds.
- If a user ID is specified:
- If the ID matches the current user → succeeds.
- Otherwise, if the current user does not have Management permission →
403 Forbidden. - If the current user has Management permission but the target user does not exist →
404 Not Found. - If the current user has Management permission and the target user exists → succeeds.
Same as below, a profile may not have skin or cape set — even if with_skin=true is specified, the returned skin may be an empty object.
Create Profile Auth required
POST /profileRequest body:
type Request = {
name: string;
}Response:
type Payload = Profile;Delete Profile Auth required
DELETE /profiles/{id}Response:
type Payload = Profile;- Returns
404 Not Foundif the target profile doesn't exist. - If the target profile exists but doesn't belong to the current user:
- If the current user has Management permission → succeeds.
- Otherwise →
403 Forbidden.
Get Profile Info
This endpoint does not require authentication.
GET /profiles/{id}?with_skin={bool}with_skin: Boolean. Whether to include skin data in the response.
Response:
type Payload = {
metadata: Profile;
skin?: ProfileSkin;
}Note that a profile may not have skin or cape set — all three fields in skin can be absent.
Update Profile Metadata Auth required
PATCH /profiles/{id}Request body:
type Request = {
name?: string
}Response:
type Payload = Profile;Authentication for this endpoint is the same as the "Delete Profile" endpoint.
The name here is a Minecraft player name, subject to the same constraints as the official Minecraft: Java Edition player names (otherwise the game will report "Invalid characters" and refuse to enter the world):
- Length between 3–16 characters;
- Only letters, digits, and underscores allowed.
If the player name being modified does not meet these requirements, 400 Bad Request is returned.
Update Texture Auth required
PUT <aphanite_base_url>/api/yggdrasil/api/user/profile/{uuid}/{textureType}
DELETE <aphanite_base_url>/api/yggdrasil/api/user/profile/{uuid}/{textureType}For implementation details and usage, refer to the Authlib-injector documentation.
All users in Aphanite can upload skins and capes for any of their profiles.