# esx_license (/docs/addons/esx_license)

`esx_license` keeps track of which licenses each player owns, for example a driving license or a weapon permit. Other resources such as [esx_dmvschool](/docs/addons/esx_dmvschool), [esx_boat](/docs/addons/esx_boat), [esx_vehicleshop](/docs/addons/esx_vehicleshop) and [esx_weaponshop](/docs/addons/esx_weaponshop) grant and check licenses through it.

## Overview [#overview]

| Fact | Value |
| --- | --- |
| Resource | `esx_license` |
| Side | Server only |
| Dependencies | `esx_lib`, `es_extended`, `oxmysql` |
| SQL | `esx_license.sql` (`licenses`, `user_licenses`) |

## Installation [#installation]

<Steps>
<Step>
### Import the SQL [#import-the-sql]

```sql title="esx_license.sql"
CREATE TABLE `licenses` (
	`type` varchar(60) NOT NULL,
	`label` varchar(60) NOT NULL,
	PRIMARY KEY (`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `user_licenses` (
	`id` int NOT NULL AUTO_INCREMENT,
	`type` varchar(60) NOT NULL,
	`owner` VARCHAR(60) NOT NULL,
	PRIMARY KEY (`id`),
	UNIQUE KEY `uq_user_licenses_owner_type` (`owner`, `type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

`licenses` is the catalog of license types. The addons that use a license insert their own rows, for example:

```sql
INSERT INTO `licenses` (`type`, `label`) VALUES ('weapon', 'Weapon License');
```
</Step>
<Step>
### Start it [#start-it]

```bash title="server.cfg"
ensure esx_license
```
</Step>
</Steps>

On start the resource checks `user_licenses` for duplicate `owner` + `type` rows. If it finds any it stops granting licenses and prints `Schema migration failed; grants disabled`. Remove the duplicates and restart. If the unique key is missing it adds it. The same migration is shipped as `sql/security_hardening_2026_10_02.sql` in the addons repo; see the [addons overview](/docs/addons).

## Configuration [#configuration]

| Key | Default | Description |
| --- | --- | --- |
| `Config.allowedJobs` | `{ ['police'] = true }` | Jobs that may read and remove other players' licenses. |
| `Config.LicenseCheckDistance` | `5.0` (not in `config.lua`) | Max distance in meters between an allowed-job player and the target. Add the key to `config.lua` to change it. |

### Who may read another player's licenses [#who-may-read-another-players-licenses]

A client request for another player passes when one of these is true:

- The caller asks about themselves.
- The caller's group is `admin` or `superadmin`.
- The caller's job is in `Config.allowedJobs`, the job's `onDuty` is not `false`, and the target is within `Config.LicenseCheckDistance`.

## Server events [#server-events]

Local events for other server scripts. Clients cannot trigger them, except `removeLicense`.

### esx_license:addLicense [#esx_licenseaddlicense]

```lua
TriggerEvent('esx_license:addLicense', playerId, licenseType, cb)
```

| Name | Type | Description |
| --- | --- | --- |
| `playerId` | number | Server ID of an online player. |
| `licenseType` | string | A `type` from the `licenses` table. |
| `cb` | function or nil | Called with `false` if the player already owns it, otherwise with the insert result. |

```lua title="server/main.lua"
TriggerEvent('esx_license:addLicense', source, 'weapon', function(result)
    if result then
        TriggerClientEvent('esx:showNotification', source, 'Weapon license issued')
    end
end)
```

<Callout type="warn">
If `licenseType` is not in the `licenses` table, the license is not added, a warning is printed and `cb` is never called.
</Callout>

### esx_license:checkLicense [#esx_licensechecklicense]

```lua
TriggerEvent('esx_license:checkLicense', playerId, licenseType, cb)
```

`cb(hasLicense)` receives a boolean. Nothing happens if the player is offline.

```lua title="server/main.lua"
TriggerEvent('esx_license:checkLicense', source, 'drive', function(hasLicense)
    if not hasLicense then
        return TriggerClientEvent('esx:showNotification', source, 'You need a driving license')
    end
    -- continue
end)
```

### esx_license:getLicenses [#esx_licensegetlicenses]

```lua
TriggerEvent('esx_license:getLicenses', playerId, cb)
```

`cb(licenses)` receives an array of `{ type, label }` for an online player.

### esx_license:getLicense [#esx_licensegetlicense]

```lua
TriggerEvent('esx_license:getLicense', licenseType, cb)
```

`cb({ type = licenseType, label = label })`. `label` is `nil` when the type does not exist. This reads the catalog; it does not check a player.

### esx_license:getLicensesList [#esx_licensegetlicenseslist]

```lua
TriggerEvent('esx_license:getLicensesList', function(list)
    for _, license in ipairs(list) do
        print(license.type, license.label)
    end
end)
```

Returns the cached catalog, loaded on start and refreshed on `esx:sqlCatalogReady`.

### esx_license:removeLicense (net event) [#esx_licenseremovelicense-net-event]

```lua
TriggerServerEvent('esx_license:removeLicense', targetId, licenseType)
```

| Name | Type | Description |
| --- | --- | --- |
| `targetId` | number | Server ID of the player who loses the license. |
| `licenseType` | string | License type. |

<Callout type="info" title="Security">
This is the only event clients can send. The server checks that the caller's job is in `Config.allowedJobs`, the license type exists, and the caller may read the target (see the rules above). Otherwise the caller gets "Your job is not allowed to remove the license".
</Callout>

To remove a license from server code, delete the row yourself:

```lua title="server/main.lua"
local xTarget = ESX.Player(targetId)

MySQL.update('DELETE FROM user_licenses WHERE type = ? AND owner = ?', {
    'weapon', xTarget.getIdentifier(),
})
```

## Client callbacks [#client-callbacks]

Call with `ESX.TriggerServerCallback`, `ESX.AwaitServerCallback` or `xLib.callback`.

| Callback | Arguments | Returns |
| --- | --- | --- |
| `esx_license:checkLicense` | `targetId`, `licenseType` | `boolean` |
| `esx_license:getLicenses` | `targetId?` | array of `{ type, label }` (empty when denied) |
| `esx_license:getLicense` | `licenseType` | `{ type, label }` |
| `esx_license:getLicensesList` | none | array of `{ type, label }` |

`targetId` defaults to the caller when omitted. Reading someone else uses the permission rules above.

```lua title="client/main.lua"
ESX.TriggerServerCallback('esx_license:checkLicense', function(hasLicense)
    if hasLicense then
        OpenGunShop()
    else
        ESX.ShowNotification('You need a weapon license')
    end
end, GetPlayerServerId(PlayerId()), 'weapon')
```

## Common issues [#common-issues]

- **addLicense does nothing**: the type is missing in `licenses`, or the player is offline. Check the server console for `Missing license type in db`.
- **Grants disabled after update**: duplicate rows in `user_licenses`. Remove them and restart.
- **Police cannot see licenses**: the officer is too far away (5 m), off duty, or the job is not in `Config.allowedJobs`.

## Related [#related]

- [esx_dmvschool](/docs/addons/esx_dmvschool)
- [esx_weaponshop](/docs/addons/esx_weaponshop)
- [esx_policejob](/docs/addons/esx_policejob)
- [Database schema](/docs/guides/database-schema)
