Framework Integration
NAC ID system and integration examples for QBCore, ESX, and Qbox.
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:
radioAccessCheck— can this player open the radio at all?- Zone
nacIds— does the player's NAC ID appear here? If not, the zone is invisible to them. - Channel
allowedNacs— can they connect and transmit? - Channel
scanAllowedNacs— can they scan (listen-only)?
Example:
- Police get NAC ID
"141"→ they see zones withnacIds = { "141" }and can connect to channels withallowedNacs = { "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:
-- 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)
endRefreshing 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:
['radio'] = {
['name'] = 'radio',
['label'] = 'Radio',
['weight'] = 1000,
['type'] = 'item',
['image'] = 'radio.png',
['unique'] = false,
['useable'] = true,
['shouldClose'] = true,
['combinable'] = nil,
['description'] = 'Handheld radio transceiver'
},-- 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)
endESX
Radio Item (Optional)
Add to your database if you want to require a radio item:
INSERT INTO `items` (`name`, `label`, `weight`, `rare`, `can_remove`) VALUES
('radio', 'Radio', 1, 0, 1);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)
endQbox (QBX Core)
Radio Item (Optional)
Add to ox_inventory/data/items.lua:
['radio'] = {
label = 'Radio',
weight = 500,
stack = false,
close = true,
client = {
event = 'radio:use',
},
},-- 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