Framework Integration

NAC ID system and integration examples for QBCore, ESX, and Qbox.

QBCoreESXQbox

NAC ID System

NAC IDs (Network Access Codes) are how the radio controls per-department access. You assign a NAC ID to each player via getUserNacId in config.lua, and that ID is checked against zones and channels to determine what they can see and use.

The access flow:

  1. radioAccessCheck — can this player open the radio at all?
  2. Zone nacIds — does the player's NAC ID appear here? If not, the zone is invisible to them.
  3. Channel allowedNacs — can they connect and transmit?
  4. Channel scanAllowedNacs — can they scan (listen-only)?

Example:

  • Police get NAC ID "141" → they see zones with nacIds = { "141" } and can connect to channels with allowedNacs = { "141" }
  • EMS get NAC ID "200" → they only see zones and channels configured for "200"
  • A channel with scanAllowedNacs = { "200" } lets EMS listen to police radio without transmitting
  • Returning "0" (or any string not in any zone/channel config) effectively gives the player no radio access — it's not a special value, it just won't match anything

Players whose NAC ID matches dispatchNacId in config.lua can also trigger alerts from the in-game SGN button. By default both are "141", so all players can trigger alerts — change dispatchNacId to a separate value to restrict this.

Core Functions

Three callbacks in config.lua control access:

config.lua
-- Who can use the radio at all (server-side)
Config.radioAccessCheck = function(playerId)
    return true  -- Default: allow everyone
end

-- Assign NAC ID based on job/role (server-side)
Config.getUserNacId = function(serverId)
    return "141"  -- Default: everyone gets "141"
end

-- Display name for radio and dispatch panel (server-side)
Config.getPlayerName = function(serverId)
    return GetPlayerName(serverId)
end

Refreshing NAC IDs

When a player's job changes, call this server-side so their channel access updates immediately without a reconnect:

-- Single player (call this on job change events)
exports['tRadio']:refreshNacId(serverId)

-- All players
exports['tRadio']:refreshPlayerInfo()

QBCore:

RegisterNetEvent('QBCore:Server:OnJobUpdate', function(source)
    exports['tRadio']:refreshNacId(source)
end)

ESX:

AddEventHandler('esx:setJob', function(source)
    exports['tRadio']:refreshNacId(source)
end)

Qbox:

RegisterNetEvent('QBCore:Server:OnJobUpdate', function(source)
    exports['tRadio']:refreshNacId(source)
end)

QBCore

Radio Item (Optional)

Add to qb-core/shared/items.lua if you want to require a radio item:

qb-core/shared/items.lua
['radio'] = {
    ['name'] = 'radio',
    ['label'] = 'Radio',
    ['weight'] = 1000,
    ['type'] = 'item',
    ['image'] = 'radio.png',
    ['unique'] = false,
    ['useable'] = true,
    ['shouldClose'] = true,
    ['combinable'] = nil,
    ['description'] = 'Handheld radio transceiver'
},
config.lua — QBCore
-- Require radio item
Config.radioAccessCheck = function(playerId)
    local Player = exports['qb-core']:GetPlayer(playerId)
    if not Player then return false end
    local radioItem = Player.Functions.GetItemByName("radio")
    return radioItem and radioItem.amount > 0 or false
end

-- Job-based access (alternative — no item required)
-- Config.radioAccessCheck = function(playerId)
--     local player = exports['qb-core']:GetPlayer(playerId)
--     if player and player.PlayerData.job then
--         local job = player.PlayerData.job.name
--         return job == "police" or job == "ambulance" or job == "ems"
--     end
--     return false
-- end

-- NAC ID by job
Config.getUserNacId = function(serverId)
    local player = exports['qb-core']:GetPlayer(serverId)
    if player and player.PlayerData.job then
        local job = player.PlayerData.job.name
        if job == "police" then return "141"
        elseif job == "ambulance" or job == "ems" then return "200"
        elseif job == "firefighter" then return "300"
        end
    end
    return "0"
end

-- Display name: callsign > lastname > FiveM name
Config.getPlayerName = function(serverId)
    local player = exports['qb-core']:GetPlayer(serverId)
    if player and player.PlayerData then
        if player.PlayerData.metadata.callsign then
            return player.PlayerData.metadata.callsign
        end
        if player.PlayerData.charinfo.lastname then
            return player.PlayerData.charinfo.lastname
        end
    end
    return GetPlayerName(serverId)
end

-- Make radio useable from inventory (optional, add to bottom of config.lua)
if IsDuplicityVersion() then
    exports['qb-core']:CreateUseableItem('radio', function(source)
        TriggerClientEvent('radio:use', source)
    end)
else
    RegisterNetEvent('radio:use', function()
        exports['tRadio']:openRadio()
    end)
end

ESX

Radio Item (Optional)

Add to your database if you want to require a radio item:

Database
INSERT INTO `items` (`name`, `label`, `weight`, `rare`, `can_remove`) VALUES
('radio', 'Radio', 1, 0, 1);
config.lua — ESX
ESX = exports['es_extended']:getSharedObject()

-- Require radio item
Config.radioAccessCheck = function(playerId)
    local xPlayer = ESX.GetPlayerFromId(playerId)
    if not xPlayer then return false end
    local radioItem = xPlayer.getInventoryItem('radio')
    return radioItem and radioItem.count > 0 or false
end

-- NAC ID by job
Config.getUserNacId = function(serverId)
    local xPlayer = ESX.GetPlayerFromId(serverId)
    if not xPlayer then return "0" end
    local job = xPlayer.getJob()
    if job.name == "police" then return "141"
    elseif job.name == "ambulance" then return "200"
    elseif job.name == "fire" or job.name == "firefighter" then return "300"
    end
    return "0"
end

-- Display name
Config.getPlayerName = function(serverId)
    if not serverId or serverId <= 0 then return "DISPATCH" end
    local xPlayer = ESX.GetPlayerFromId(serverId)
    if not xPlayer then return GetPlayerName(serverId) end

    local callsign = xPlayer.getMeta("callsign")
    if callsign and callsign ~= "" then return callsign end

    local characterName = xPlayer.getName()
    if characterName and characterName ~= "" then
        local lastName = string.match(characterName, "%s+(%S+)$")
        if lastName then return lastName end
        return characterName
    end
    return GetPlayerName(serverId)
end

-- Make radio useable from inventory (optional, add to bottom of config.lua)
if IsDuplicityVersion() then
    ESX.RegisterUsableItem('radio', function(playerId)
        TriggerClientEvent('radio:use', playerId)
    end)
else
    RegisterNetEvent('radio:use', function()
        exports['tRadio']:openRadio()
    end)
end

Qbox (QBX Core)

Radio Item (Optional)

Add to ox_inventory/data/items.lua:

ox_inventory/data/items.lua
['radio'] = {
    label = 'Radio',
    weight = 500,
    stack = false,
    close = true,
    client = {
        event = 'radio:use',
    },
},
config.lua — Qbox
-- Require radio item (ox_inventory)
Config.radioAccessCheck = function(playerId)
    local count = exports.ox_inventory:Search(playerId, 'count', 'radio')
    return count and count > 0 or false
end

-- NAC ID by job
Config.getUserNacId = function(serverId)
    local player = exports.qbx_core:GetPlayer(serverId)
    if player and player.PlayerData.job then
        local job = player.PlayerData.job.name
        if job == "police" then return "141"
        elseif job == "ambulance" or job == "ems" then return "200"
        elseif job == "firefighter" then return "300"
        end
    end
    return "0"
end

-- Display name: callsign > lastname > FiveM name
Config.getPlayerName = function(serverId)
    if not serverId or serverId <= 0 then return "DISPATCH" end
    local player = exports.qbx_core:GetPlayer(serverId)
    if player and player.PlayerData then
        if player.PlayerData.metadata and player.PlayerData.metadata.callsign
           and player.PlayerData.metadata.callsign ~= "NO CALLSIGN"
           and player.PlayerData.metadata.callsign ~= "" then
            return player.PlayerData.metadata.callsign
        end
        if player.PlayerData.charinfo and player.PlayerData.charinfo.lastname
           and player.PlayerData.charinfo.lastname ~= "" then
            return player.PlayerData.charinfo.lastname
        end
    end
    return GetPlayerName(serverId)
end

-- Make radio useable from inventory (optional, add to bottom of config.lua)
if IsDuplicityVersion() then
    exports.qbx_core:CreateUseableItem('radio', function(source)
        TriggerClientEvent('radio:use', source)
    end)
else
    RegisterNetEvent('radio:use', function()
        exports['tRadio']:openRadio()
    end)
end

On this page

Need help?

Ask on Discord