HELIX Docs
Platform API

Universal Avatar

Cross-runtime avatar loadouts, equipment slots, and placeholder-compatible socket rules for Web and Unreal.

Universal Avatar is the player-owned character layer that travels across HELIX runtimes. The same loadout should be readable by the Web game, the Web SDK, and the Unreal runtime, even when an item ships different renderable assets per runtime.

This page is the shared contract for the first Character Creator implementation.

Loadout model

The platform stores one loadout per player:

type AvatarBaseShape = 'male' | 'female' | 'custom';

type UniversalAvatarBody =
  | {
      mode: 'preset';
      baseMeshId: 'helix:humanoid:male' | 'helix:humanoid:female';
    }
  | {
      mode: 'customMesh';
      avatarItemId: string;
      inventoryItemId: string;
      glbUrl: string | null;
      skeleton: string | null;
    };

type AvatarLoadout = {
  version: 1;
  body: UniversalAvatarBody;
  baseShape: AvatarBaseShape;
  customAvatar: EquippedAvatar; // { source: 'equipped' | 'auto' | 'default' | 'trial', itemId, glbUrl, skeleton, … }
  equippedSlots: Array<{
    slot: string;            // a Cosmetic.Slot.* tag
    itemId: string;
    inventoryItemId: string;
    socket?: string | null;
    inventoryItem: InventoryItem;
  }>;
  inventory: InventoryItem[];
  updatedAt: string;
};

Presets reference humanoid meshes shipped with the runtime, so they need no fetch. custom uses an owned avatar item and points to its web-compatible GLB when available. Character body editing is locked while a custom full mesh is active.

baseShape is derived, not stored: it is custom when a custom avatar is equipped and male otherwise. There is no female base mesh yet, so a patch with baseShape: 'female' is rejected with a 400, and the preset body always resolves to helix:humanoid:male. Change the body through customAvatarInventoryItemId.

Equipment slots

Avatar equipment is instance-based. Equip by inventory instance id, not universal item id, so duplicate owned copies can be selected independently. An item can only be equipped into the slot it declares.

Slot keys are Character Creator tags (Cosmetic.Slot.*), the same vocabulary as an item's own slot field, and responses always return them. The older camelCase names are still accepted on a patch (deprecated) and translated one way. headPreset, faceType and clothingPreset are body presets with no tag and are stored as-is.

Legacy nameSlot tagWeb engine default socket
headPreset— (preset)head
faceType— (preset)head
eyebrowsCosmetic.Slot.Appearance.Eyes.Eyebrowshead
eyelashesCosmetic.Slot.Appearance.Eyes.Eyelasheshead
irisesCosmetic.Slot.Appearance.Eyes.Irishead
hairCosmetic.Slot.Appearance.Hair.Mainhead
clothingPreset— (preset)chest
topCosmetic.Slot.Clothing.Topchest
outfitCosmetic.Slot.Clothing.Setchest
bottomCosmetic.Slot.Clothing.Bottomships_l
underwearCosmetic.Slot.Clothing.Underwear.Bottomhips_l
socksCosmetic.Slot.Clothing.Sockships_l
shoesCosmetic.Slot.Clothing.Shoeships_l
bagCosmetic.Slot.Clothing.Backpackback
hatCosmetic.Slot.Accessory.Head.Hathead
maskCosmetic.Slot.Accessory.Face.Maskhead
eyewearCosmetic.Slot.Accessory.Face.Eyewearhead
necklaceCosmetic.Slot.Accessory.Neck.Necklacechest
nailsCosmetic.Slot.Accessory.Hands.Nailshand_r.grip
glovesCosmetic.Slot.Accessory.Hands.Gloveshand_r.grip

The backend reports a slot's socket from the inventory instance's state, then its metadata, then the item's properties.universalAvatar.socket, and null otherwise; the web engine then falls back to the default socket above.

Platform API

Read the current player's loadout:

GET /api/v1/universal-items/avatar/loadout/me
Authorization: Bearer <access token>

Patch the current player's loadout:

PATCH /api/v1/universal-items/avatar/loadout/me
Authorization: Bearer <access token>
Content-Type: application/json

{
  "customAvatarInventoryItemId": null,
  "equippedSlots": {
    "Cosmetic.Slot.Clothing.Top": "inventory-instance-id",
    "Cosmetic.Slot.Clothing.Shoes": null
  }
}

null unequips a slot (or, for customAvatarInventoryItemId, clears the custom avatar). Unknown slots, an item equipped into a slot it doesn't declare, and non-wearable inventory instances are rejected. The old placeholder-seed route is retired and answers 410 Gone.

Web SDK and shell behavior

The Character Creator is a shell overlay, not a standalone destination. It can be raised from any home or world that allows Universal Avatars. Worlds can request the shell to open the creator and can read or patch loadouts:

await Helix.avatar.openCreator();

const loadout = await Helix.avatar.getLoadout();

await Helix.avatar.updateLoadout({
  equippedSlots: {
    bag: backpackInventoryInstanceId,
  },
});

const off = Helix.avatar.onAvatarChanged(async () => {
  applyAvatar(await Helix.avatar.getLoadout()); // the callback carries no payload: re-read
});

The shell remains authoritative for authenticated API calls. Direct local world runs may return an empty/default loadout until they are embedded in the HELIX shell. When avatar.updateLoadout succeeds, the shell emits avatar-changed and equipment-changed to the active world, which fire onAvatarChanged.

For rendering, Helix.avatar.getEquipped() resolves the local player's configured avatar and getEquippedForUser(userId) another joined player's public one, for replicas. projectResource and readResource read authorized avatar bytes through the shell.

Engine runtime

The Web engine uses a humanoid equipment service over the existing socket service:

  1. Resolve the item slot.
  2. Resolve the socket from properties.universalAvatar.socket, item metadata, or the default slot map.
  3. Attach the item's renderable mesh to that socket.
  4. Use a generated primitive placeholder when the item has no web-compatible mesh yet.

Placeholder meshes must still follow the shared skeleton/socket contract. They are temporary renderables, not a separate ownership or equipment model.

When a world uses the HELIX humanoid as the player character and Universal Avatars are enabled, the runtime should read Helix.avatar.getLoadout() during player setup and apply the returned loadout to the humanoid by default. Worlds can opt into additional scripting later, but the baseline humanoid path should not require custom per-world avatar code.

Unreal compatibility

The Unreal runtime should consume the same loadout fields and slot ids. Runtime-specific mesh assets may differ, but slot names, inventory instance ids, base shape semantics, and custom-avatar lock behavior should not diverge. Today the Unreal plugin's avatar.getEquipped, getEquippedForUser, openCreator and onAvatarChanged are available; getLoadout and updateLoadout work only through the shell until the runtime token is accepted.

On this page