FiveM Docs
Core Resources

esx_skin

esx_skin is the ESX Legacy character creator and appearance saver. Server exports, client menu events, config keys, the /skin command and save errors explained.

esx_skin is the ESX Legacy character creator. It opens a Svelte NUI on top of skinchanger, lets players edit their ped, and stores the result in users.skin. Use its server exports when your own resource (clothing shop, barber, outfit wardrobe) needs to read or save a player's appearance.

Overview

Resourceesx_skin
Version1.16.3 (fxmanifest, ships with ESX Legacy 1.16.4)
SideClient (menu, camera) + server (validation, storage)
Dependencieses_extended, skinchanger, esx_lib, oxmysql
Databaseusers.skin (JSON)

How saving works

1.16 moved all skin storage to the server and hardened it:

  • Every skin is validated against Config.SkinFields. Unknown keys, non-integers or out-of-range values are rejected.
  • Clients can only save through esx_skin:save while they have an edit session. Sessions are created on the first login of a player without a skin, by the /skin command, and by the BeginSkinEdit export. A session expires after Config.SkinEditSessionTTL and is consumed by a successful save.
  • Saves are rate limited per player (Config.SkinSaveCooldown) and queued per identifier.
  • Shops should save on the server with a scope (clothes, barber, outfit) so the player cannot change fields the shop does not sell.

Configuration

KeyDefaultDescription
Config.LocaleGetConvar('esx:locale', 'en')Language.
Config.BackpackWeight{ [40] = 16, [41] = 20, [44] = 25, [45] = 23 }Extra carry weight per bags_1 drawable. Ignored when es_extended uses a custom inventory.
Config.AllowClothingBackpackCapacityfalsetrue: any worn bag adds weight. false: only bags granted with SetBackpackCapacity count.
Config.SkinMaxBytes8192Maximum JSON size of a skin.
Config.SkinSaveCooldown2000Rate limiter refill interval for saves, in ms (burst of 2).
Config.SkinCacheTTL60000How long a loaded skin stays cached, in ms.
Config.SkinFieldstableAllowed skin keys with { min, max }, for example sex = { 0, 1 }, mom = { 0, 45 }, nose_1 = { -10, 10 }, torso_1 = { 0, 4095 }.
Config.SkinEditSessionTTL900000Lifetime of an edit session, in ms (15 minutes).
Config.SkinEditFieldsbuilt in configField whitelists per scope: clothes, barber, outfit.

Scopes are built in config.lua:

ScopeFields
clothestshirt, torso, decals, pants, shoes, bags, chain, helmet, glasses, watches (_1 and _2), arms, arms_2
barberbeard_1-4, eyebrows_1-4, makeup_1-4, lipstick_1-4, hair_1, hair_2, hair_color_1, hair_color_2, ears_1, ears_2
outfitall clothes fields plus mask, bproof, bracelets, ears (_1 and _2)

If you add custom components to skinchanger, also add them to Config.SkinFields. Otherwise every save fails with invalid_appearance:<field>:unknown_field.

Server exports

SaveSkin

exports.esx_skin:SaveSkin(playerId, skin, callback, scope)
NameTypeDescription
playerIdnumberServer ID of a loaded player.
skintableFull skin table (must include sex).
callbackfun(success: boolean, reason?: string) | nilCalled when the write finished or failed.
scope"clothes" | "barber" | "outfit" | nilLimits which fields may differ from the stored skin. nil saves the whole skin.

Returns false when the request was rejected immediately, otherwise true (queued). With barber, only barber fields are copied onto the stored skin. With clothes or outfit, any change outside the scope is rejected with appearance_scope_mismatch:<field>.

server/main.lua
xLib.callback.register('myshop:buyHaircut', function(source, skin)
    local p = promise.new()
    exports.esx_skin:SaveSkin(source, skin, function(success, reason)
        p:resolve({ success = success, reason = reason })
    end, 'barber')
    return Citizen.Await(p)
end)

SaveOutfit

exports.esx_skin:SaveOutfit(playerId, clothes, callback)
NameTypeDescription
playerIdnumberServer ID.
clothestableClothing values. Only outfit scope fields are copied; everything else is ignored.
callbackfun(saved: boolean, skin?: table) | nilReceives the merged skin on success.
server/main.lua
local policeUniform = { tshirt_1 = 58, tshirt_2 = 0, torso_1 = 55, torso_2 = 0, arms = 41, pants_1 = 25, pants_2 = 0, shoes_1 = 25, shoes_2 = 0 }

exports.esx_skin:SaveOutfit(source, policeUniform, function(saved, skin)
    if saved then
        TriggerClientEvent('skinchanger:loadSkin', source, skin)
    end
end)

GetSkin

local skin, ready = exports.esx_skin:GetSkin(playerId)

Returns the stored skin table (or nil) and ready, which is false when the player is not loaded or the database read failed. Uses the cache when possible.

BeginSkinEdit

local ok = exports.esx_skin:BeginSkinEdit(playerId)

Opens an edit session so the client may save its full skin once through the saveable menu. Returns false if the player is not loaded or a save is in flight.

server/main.lua
RegisterCommand('plasticsurgery', function(source)
    if exports.esx_skin:BeginSkinEdit(source) then
        TriggerClientEvent('esx_skin:openSaveableMenu', source)
    end
end, true)

ValidateSkin

local clean = exports.esx_skin:ValidateSkin(skin)

Returns a decoded copy of the skin if it passes Config.SkinFields validation, otherwise nil.

SetBackpackCapacity

local ok = exports.esx_skin:SetBackpackCapacity(playerId, bag)
NameTypeDescription
playerIdnumberServer ID.
bagnumber | nilA key from Config.BackpackWeight, or nil to remove the bonus.

Stores authorizedBackpack in the player's metadata and sets max weight to ESX MaxWeight + Config.BackpackWeight[bag]. Returns false for unknown bags.

Server callback

esx_skin:getPlayerSkin returns the stored skin and the job outfits:

client/main.lua
xLib.callback('esx_skin:getPlayerSkin', false, function(skin, jobSkin)
    if skin then
        TriggerEvent('skinchanger:loadSkin', skin)
    end
    -- jobSkin = { skin_male = ..., skin_female = ... } from the player's job grade
end)

It is also registered as a compat callback, so ESX.TriggerServerCallback('esx_skin:getPlayerSkin', cb) keeps working.

Client events

All menu events take Lua functions as arguments, so trigger them locally with TriggerEvent (or from the server without callbacks).

EventArgumentsDescription
esx_skin:openMenusubmitCb, cancelCbOpens the editor without saving.
esx_skin:openRestrictedMenusubmitCb, cancelCb, restrictSame, limited to the component names in restrict.
esx_skin:openSaveableMenusubmitCb?, cancelCb?Opens the editor and saves on submit. Needs an edit session.
esx_skin:openSaveableRestrictedMenusubmitCb?, cancelCb?, restrictSaveable and restricted.
esx_skin:getLastSkincb(skin)Returns the skin before the menu was opened.
esx_skin:setLastSkinskinOverrides the last skin, for example after a paid change.
esx_skin:playerRegisterednoneFired by esx_identity; loads the saved skin or opens the creator.
esx_skin:resetFirstSpawnnoneMarks the next spawn as first spawn (used by identity and multicharacter).
esx_skin:saveFailednoneServer tells the client the legacy esx_skin:save net event failed.

submitCb(data, menu) and cancelCb(data, menu) receive data.current (focused element), data.elements, and a menu handle with close(), update(_, element) and refresh(). When you cancel, the last skin is restored automatically.

client/main.lua
-- shop: only show clothes, let the server decide whether to save
TriggerEvent('esx_skin:openRestrictedMenu', function(data, menu)
    menu.close()
    TriggerEvent('skinchanger:getSkin', function(skin)
        TriggerServerEvent('myshop:buyClothes', skin) -- server calls SaveSkin(..., 'clothes')
    end)
end, function(data, menu)
    menu.close()
end, { 'tshirt_1', 'tshirt_2', 'torso_1', 'torso_2', 'arms', 'pants_1', 'pants_2', 'shoes_1', 'shoes_2' })

Never add an unauthenticated server event that saves whatever skin the client sends. Validate payment or permission first, then call SaveSkin with a scope.

Commands

CommandPermissionArgumentsDescription
/skinadminplayerId (optional)Opens a saveable editor for yourself or the target player.

Common issues

  • "Your appearance could not be saved". Check the F8 console for [esx_skin] Appearance save failed: <reason>. edit_session_missing means no BeginSkinEdit was called; invalid_appearance:... means a field is unknown or out of range; rate_limited means saves come too fast.
  • Old shop scripts stop saving. Scripts that open esx_skin:openSaveableMenu or fire esx_skin:save from the client without a server-side BeginSkinEdit no longer work. Move the save to the server and use SaveSkin with a scope.
  • Backpack weight does not apply. With Config.AllowClothingBackpackCapacity = false you must call SetBackpackCapacity. It never applies when es_extended uses ox_inventory or another custom inventory.
  • esx_skin:setWeight does nothing. It is kept as an empty handler for compatibility.

Frequently asked questions

Independent documentation

esx-fivem.com is a community project and is not affiliated with the ESX Framework team. Facts on this page were checked against the upstream source. Official resources: docs.esx-framework.org and github.com/esx-framework.
Edit on GitHub

Last updated on

On this page