Skip to Content
ScriptingMigrating from Deprecated APIs

Migrating from Deprecated APIs

FiveM’s scripting APIs evolve, and some older patterns still run but no longer match the current recommended approach. This page covers two common ones: chat message events and command registration. It is not about framework migration — see Framework Migration for moving between ESX, QBCore, and Qbox.

Chat messages: chatMessage event to chat:addMessage

Older resources trigger the client-side chatMessage event directly, or call TriggerClientEvent('chatMessage', ...) with positional arguments for author, color, and text. The built-in chat resource now expects a structured message object sent through chat:addMessage, which supports a template, per-channel color, multiline text, and named args instead of hand-formatted strings.

Before

-- Deprecated: positional args, no structure, easy to get the order wrong TriggerClientEvent('chatMessage', playerSource, '', { 255, 0, 0 }, '^1Server: ^0' .. message)
-- Deprecated: raw chatMessage broadcast to everyone TriggerClientEvent('chatMessage', -1, 'Server', { 255, 0, 0 }, message)

After

TriggerClientEvent('chat:addMessage', playerSource, { color = { 255, 0, 0 }, multiline = true, args = { 'Server', message } })
-- Broadcast to everyone TriggerClientEvent('chat:addMessage', -1, { template = '<div class="chat-message server"><b>{0}</b><br>{1}</div>', args = { 'Server', message } })

args values are inserted positionally into the default template (or your custom template), and are HTML-escaped by the chat resource — you don’t need to manually strip player-supplied text before sending it as an arg. Keep building the message object on the server so a modified client can’t forge the sender name or color.

Command registration: unrestricted RegisterCommand to ACE-aware restriction

Older command code registers a command and either performs no permission check at all, or only hides the command from suggestions on the client — which does not stop a modified client or a direct console call from executing it. RegisterCommand takes a third restricted argument that ties the command to ACE permissions and is enforced by the game itself before your handler runs.

Before

-- Deprecated: no restriction argument, anyone can run it RegisterCommand('kick', function(source, args) local target = tonumber(args[1]) if target then DropPlayer(target, 'Kicked by staff') end end) -- Deprecated: hiding the command client-side is not access control TriggerEvent('chat:addSuggestion', '/kick', 'Kick a player (staff only)')

Hiding a command from chat:addSuggestion only removes it from the autocomplete list. The command is still fully callable by typing it directly or invoking it through the client console, so it is not a security boundary on its own.

After

RegisterCommand('kick', function(source, args) local target = tonumber(args[1]) if not target then return end local targetPed = GetPlayerPed(target) if targetPed == 0 then return end DropPlayer(target, 'Kicked by staff') end, true) -- restricted = true -- Suggestion is still useful for discoverability, but is not the access check TriggerEvent('chat:addSuggestion', '/kick', 'Kick a player (staff only)')

With restricted set to true, the game checks the calling player’s ACE permissions for the command.kick object before the handler runs at all, so an unauthorized call never reaches your Lua code. Grant that permission through your server’s server.cfg — see Permissions for the full ACE model:

add_ace group.admin command.kick allow add_principal identifier.license:yourlicensehash group.admin

Pair the restricted flag with server-side validation of the arguments themselves (target exists, target is not already offline, and so on) — ACE restriction controls who can call the command, not whether the arguments it receives are valid.