# esx_inventory (/docs/core/esx_inventory)

`esx_inventory` is the default slot-based inventory UI for ESX Legacy. It opens with `F2`, shows items, money and weapons from es_extended, and offers a small server API so other resources can show a second "storage" panel (stashes, society stock, trunks). es_extended remains the authority on items and weight; this resource is the presentation layer.

## Overview [#overview]

| Fact | Value |
| --- | --- |
| Resource | `esx_inventory` |
| Side | Client UI (React NUI) + server storage bridge |
| Dependencies | `es_extended`, `esx_lib` |
| Database | None (slot layout is stored in client KVP `esx_inventory:slots`) |
| Version | 1.16.3 (shipped with ESX Legacy 1.16.4) |

Features verified in the source:

- Slot grid with drag and drop. The layout is saved per player with `SetResourceKvp`.
- Use, give to a nearby player and drop items, routed through the validated es_extended events (`esx:useItem`, `esx:giveInventoryItem`, `esx:removeInventoryItem`).
- Give the ammo of a selected weapon to a nearby player.
- Cash, black money and weapons shown next to items.
- Hotbar styling on the first slots with keybinds that use the item in that slot.
- Toasts for gained and lost items, even while the inventory is closed.
- Optional second panel for storages registered by other resources.

<Callout type="warn" title="Not compatible with ox_inventory">
es_extended checks `GetResourceState("ox_inventory") ~= "missing"` and sets `Config.CustomInventory = "ox"`, which disables `EnableDefaultInventory`. esx_inventory then stops with `ESX Default Inventory is disabled in config, please enable it to use this resource.` If you use ox_inventory, do not ensure esx_inventory. See [ox_inventory with ESX](/docs/guides/ox-inventory).
</Callout>

## Installation [#installation]

esx_inventory is part of the `[core]` folder. Start it after its dependencies:

```bash title="server.cfg"
ensure oxmysql
ensure esx_lib
ensure es_extended
ensure esx_inventory
```

The UI ships prebuilt in `web/dist`. You only need to rebuild it if you change `web/src`:

```bash
cd esx_inventory/web
pnpm install
pnpm build
```

## Configuration [#configuration]

All keys live in `config/main.lua`.

| Key | Default | Description |
| --- | --- | --- |
| `Locale` | `GetConvar("esx:locale", "en")` | Language used for UI strings and notifications. |
| `PlayerSlots` | `25` | Number of slots in the player grid. Slot indexes saved in KVP are clamped to this range. |
| `HotbarSlots` | `5` | Number of first slots styled as hotbar. One keybind is registered per hotbar slot. |
| `ItemImageUrl` | `"nui://esx_inventory/web/images/%s.png"` | Image URL template. `%s` is replaced with the item, account or weapon name. |
| `NearbyPlayerDistance` | `3.0` | Max distance (meters) for a player to appear in the "give" target list. |
| `StorageBurst` | `5` | Rate limiter capacity for storage put/take actions per player. |
| `StorageRefillInterval` | `1000` | Interval (ms) after which the storage rate limiter refills `StorageBurst` tokens. |

### Theme convars [#theme-convars]

The panel reads the shared ESX UI convars through `xLib.colors.getESXTheme()`. Set them with `setr` so the client can read them:

```bash title="server.cfg"
setr esx:ui:primaryColor "#fb9b04"
setr esx:ui:logoUrl "https://example.com/logo.png"
```

Supported convars: `esx:ui:primaryColor`, `esx:ui:secondaryColor`, `esx:ui:backgroundColor`, `esx:ui:accentColor`, `esx:ui:logoUrl`.

### Item images [#item-images]

Put `itemname.png` files (for example `bread.png`, `WEAPON_PISTOL.png`, `money.png`) into `web/images/`. Missing images fall back to an icon. To use a CDN, change `ItemImageUrl` to something like `"https://cdn.example.com/items/%s.png"`.

## Keybinds [#keybinds]

Keybinds are registered with `xLib.addKeybind`, so players can rebind them in **Settings > Key Bindings > FiveM**.

| Name | Default key | Action |
| --- | --- | --- |
| `showinv` | `F2` | Open or close the inventory. |
| `hotbar1` ... `hotbar5` | `1` ... `5` | Use the item in hotbar slot 1-5. Weapons are equipped or holstered. |

While a hotbar key is held, the matching GTA weapon-wheel shortcut control is disabled so the key does not also switch weapons. The inventory does not open while the player is dead, the pause menu is active, or the player is not loaded yet.

## Client exports [#client-exports]

### ShowInventory [#showinventory]

```lua
exports.esx_inventory:ShowInventory()
```

Opens the inventory for the local player. Does nothing if it is already open, the NUI is not ready, the player is dead or the pause menu is open.

```lua title="client/main.lua"
RegisterCommand('inv', function()
    exports.esx_inventory:ShowInventory()
end, false)
```

## Server exports (storage API) [#server-exports-storage-api]

A storage is a second panel next to the player inventory. The registering resource owns the data; esx_inventory handles the player side (remove before put, carry check before and after take, refund on failure) and runs one transaction at a time per player.

### RegisterStorage [#registerstorage]

```lua
exports.esx_inventory:RegisterStorage(storageId, definition) -- returns boolean
```

| Name | Type | Description |
| --- | --- | --- |
| `storageId` | `string` | Unique, non-empty storage id. |
| `definition.label` | `string` | Title shown on the panel. Required. |
| `definition.canAccess` | `fun(xPlayer): boolean` | Access check. Required. |
| `definition.getItems` | `fun(xPlayer): table[]` | Returns `{ name, count, label?, weight? }` entries. Required. |
| `definition.putItem` | `fun(xPlayer, itemName, count): boolean` | Adds to your storage. Return `true` only after the state changed. Required. |
| `definition.takeItem` | `fun(xPlayer, itemName, count): boolean` | Removes from your storage. Return `true` only after the state changed. Required. |
| `definition.slots` | `number?` | Max distinct items. Default `30`. |
| `definition.maxWeight` | `number?` | Max total weight, `0` disables the check. Default `0`. Weights come from the es_extended item registry. |
| `definition.coords` | `vector3?` | Optional location. Transfers are refused when the player is too far away. |
| `definition.distance` | `number?` | Allowed distance from `coords`. Default `5.0`. |

Returns `false` when the id is invalid, a callback is missing, or another resource already owns the id. The owning resource may call it again to replace its definition; open viewers are refreshed. Only `item_standard` entries are supported in storages: money and weapons cannot be stored through this API.

<Callout type="warn" title="Callbacks must not yield">
The callbacks run synchronously in your resource. Do not call `MySQL.query.await`, `Wait` or other yielding functions inside them. Keep the storage contents in memory and persist them separately.
</Callout>

```lua title="server/storage.lua"
local stock = {} -- [itemName] = count, load it from your database on start

local function toList()
    local list = {}
    for name, count in pairs(stock) do
        list[#list + 1] = { name = name, count = count }
    end
    return list
end

local function registerStorages()
    exports.esx_inventory:RegisterStorage('society_mechanic', {
        label = 'Mechanic Stock',
        slots = 30,
        maxWeight = 200,
        coords = vector3(-342.29, -133.37, 39.0),
        distance = 5.0,
        canAccess = function(xPlayer)
            return xPlayer.getJob().name == 'mechanic'
        end,
        getItems = function()
            return toList()
        end,
        putItem = function(_, itemName, count)
            stock[itemName] = (stock[itemName] or 0) + count
            return true
        end,
        takeItem = function(_, itemName, count)
            local current = stock[itemName] or 0
            if current < count then return false end
            stock[itemName] = current - count > 0 and current - count or nil
            return true
        end,
    })
end

AddEventHandler('esx_inventory:ready', registerStorages)

if GetResourceState('esx_inventory') == 'started' then
    registerStorages()
end
```

### OpenStorage [#openstorage]

```lua
exports.esx_inventory:OpenStorage(playerId, storageId) -- returns boolean
```

| Name | Type | Description |
| --- | --- | --- |
| `playerId` | `number` | Server id of the player. |
| `storageId` | `string` | Id passed to `RegisterStorage`. |

Returns `false` if the storage does not exist, the player is not loaded, out of range, `canAccess` returns false, or `getItems` errors. The same function is also bound to the **local server event** `esx_inventory:openStorage`, so `TriggerEvent('esx_inventory:openStorage', playerId, storageId)` works from other server scripts.

```lua title="server/main.lua"
RegisterNetEvent('mechanic:openStock', function()
    local src = source
    if not exports.esx_inventory:OpenStorage(src, 'society_mechanic') then
        local xPlayer = ESX.GetPlayerFromId(src)
        if xPlayer then xPlayer.showNotification('You cannot open this storage.') end
    end
end)
```

### RefreshStorage [#refreshstorage]

```lua
exports.esx_inventory:RefreshStorage(storageId) -- returns boolean
```

Pushes the current `getItems` result to everyone viewing the storage. Call it after you change the contents outside of `putItem` / `takeItem` (for example from an admin command). Viewers who lost access or moved out of range get the panel closed.

### UnregisterStorage [#unregisterstorage]

```lua
exports.esx_inventory:UnregisterStorage(storageId) -- returns boolean
```

Removes a storage and closes it for every viewer. Storages are also unregistered automatically when the owning resource stops.

## Events [#events]

| Event | Side | Type | Description |
| --- | --- | --- | --- |
| `esx_inventory:ready` | Server | Local | Fired when esx_inventory starts. Register your storages again here. |
| `esx_inventory:openStorage` | Server | Local | `(playerId, storageId)`, same as `OpenStorage`. |
| `esx_inventory:storagePut` | Server | Net (internal) | Sent by the UI. Rate limited and validated. |
| `esx_inventory:storageTake` | Server | Net (internal) | Sent by the UI. Rate limited and validated. |
| `esx_inventory:storageClosed` | Server | Net (internal) | Sent when the player closes the storage panel. |
| `esx_inventory:openStorage` / `refreshStorage` / `closeStorage` | Client | Net (internal) | Sent by the server to drive the storage panel. |

Do not trigger the internal net events yourself; use the exports. The UI refreshes itself on the es_extended events `esx:setInventory`, `esx:addInventoryItem`, `esx:removeInventoryItem`, `esx:addLoadoutItem`, `esx:removeLoadoutItem`, `esx:setAccountMoney` and `esx:playerLoaded`, and closes on `esx:onPlayerDeath`.

## Common issues [#common-issues]

<Accordions>
<Accordion title="Error: ESX Default Inventory is disabled in config">
`Config.CustomInventory` is not `false` in es_extended. Most often `ox_inventory` exists in your resources folder (even if not started, the check is `~= "missing"`). Remove it or stop using esx_inventory.
</Accordion>
<Accordion title="Pressing 1-5 uses items instead of switching weapons">
The hotbar keybinds default to `1`-`5` and suppress the weapon-wheel shortcut while held. Players can unbind or rebind them in the FiveM key binding settings.
</Accordion>
<Accordion title="Storage does not reappear after restarting esx_inventory">
Storages live in memory of esx_inventory. Register them in an `esx_inventory:ready` handler as shown above so they come back after a restart.
</Accordion>
<Accordion title="Put or take silently fails">
`putItem`/`takeItem` must return exactly `true`. Any other value, or an error, counts as failure and the player's items are refunded. Also check `slots`, `maxWeight`, `coords`/`distance` and the rate limit (`StorageBurst` actions per `StorageRefillInterval`).
</Accordion>
</Accordions>

## Related [#related]

- [Core resources overview](/docs/core)
- [esx_lib](/docs/core/esx_lib)
- [Adding items](/docs/guides/add-items)
- [ox_inventory with ESX](/docs/guides/ox-inventory)
- [es_extended config](/docs/es_extended/config)
