Commands#
Command | Who | What it does |
|---|---|---|
| Everyone | Opens or closes the phone. Bound to F1 ( |
| Everyone | Answer (Q) or decline (E) an incoming call with the phone closed. |
Documentation
Chat and console commands, client and server exports, events, Dynamic Island and the Call Interception API.
Command | Who | What it does |
|---|---|---|
| Everyone | Opens or closes the phone. Bound to F1 ( |
| Everyone | Answer (Q) or decline (E) an incoming call with the phone closed. |
| Everyone | Hold to talk to Aura (B). Needs the Aura DLC. |
| Everyone | Sends an Emergency SOS to on-duty medics. |
| Jobs in | Sends a bill to a player. Only with the built-in billing. |
| Admins, console | Gives or removes the verified badge on |
| Admins, console | Prints how every online player's phone number is resolved. Use it when players report sharing a number. |
| Console | Shows the cached Cloudflare TURN servers and when they expire. |
| Console | Fetches new Cloudflare TURN credentials now. |
With Config.Debug = true a few test commands are also available: /phonecall (a test incoming call), /phonenotify <id|number> [app] [message] and /phoneseed (fills your phone with sample data). Turn debug off on a live server.
Export | Returns | What it does |
|---|---|---|
| boolean | Whether the phone is open. |
| — | Blocks the phone ( |
| boolean | Whether the phone is blocked. |
| boolean | Opens an app while the phone is open. |
| table | Starts an |
| string |
|
| — | Adds a charger inside a house. |
| — | Removes it. |
-- Block the phone while the player is in jail
exports['qs-smartphone']:ToggleDisabled(true)
-- Start a call from your script
local result = exports['qs-smartphone']:call('555-0100', 'audio', {
callerNumberOverride = '555-9999', -- optional
})
if not result.success then
print(result.error, result.message)
end
-- House charger for qs-housing
exports['qs-smartphone']:BatteryRegisterHousingCharger(houseId, vector3(x, y, z), 2.0)Show your own pill in the Dynamic Island. It competes with the phone's own by priority: calls 100, nearby sharing 90, timers 70, music 60, custom 50 by default.
exports['qs-smartphone']:showDynamicIsland({
id = 'delivery-status',
icon = 'tram_fill', -- Framework7 icon name
title = 'Delivery',
subtitle = 'On the way',
color = '#22c55e',
progress = 45, -- optional 0-100 bar
priority = 65,
duration = 8000, -- optional auto-hide (ms)
persistent = true, -- stays when the phone closes
expandedContent = {
lines = { { label = 'ETA', value = '4 min' } },
actions = { { id = 'track', label = 'Track', icon = 'map' } },
},
onClick = function() end,
onDismiss = function() end,
})
exports['qs-smartphone']:updateDynamicIsland('delivery-status', { progress = 80 })
exports['qs-smartphone']:hideDynamicIsland('delivery-status')
AddEventHandler('phone:dynamicIsland:action', function(islandId, actionId)
if islandId == 'delivery-status' and actionId == 'track' then
-- set a waypoint
end
end)Also available: getDynamicIsland(id), getAllDynamicIslands() and hasDynamicIsland(). Islands are removed when your resource stops.
Export | What it does |
|---|---|
| Registers one app. Returns |
| Registers several apps at once. |
| Changes fields, for example |
| Removes an app. |
| Lists every registered app. |
-- The player's active number
local number = exports['qs-smartphone']:GetCurrentPhoneNumber(source)
-- The player's phone id (device), needed for notifications by scope
local scopeId = exports['qs-smartphone']:getPhoneScopeIdentifier(source)
-- Push notification to a player
exports['qs-smartphone']:sendPhoneNotification(source, {
appId = 'my_app',
title = 'New order',
text = 'Your package is ready for pickup.',
closeTimeout = 5000,
})
-- Same, by phone id
exports['qs-smartphone']:sendPhoneNotificationToScope(scopeId, { appId = 'my_app', title = 'Hello', text = 'World' })
-- Block or allow a player's phone
exports['qs-smartphone']:ToggleDisabled(source, true)On failure the notification exports return false and an error code: INVALID_SOURCE, NO_SCOPE (the player has no active phone) or INVALID_PAYLOAD.
SendNewMessageFromApp puts a text in the Messages app from a sender you name (a bank, City Hall, your job). The message is saved even if the player is offline.
-- Short form: by number or by player id
exports['qs-smartphone']:SendNewMessageFromApp('555-0100', 'Bank', 'Your account has been credited with $500.')
exports['qs-smartphone']:SendNewMessageFromApp(source, 'Job Center', 'Your application has been approved.')
-- Full form
local result = exports['qs-smartphone']:SendNewMessageFromApp({
source = source, -- or targetPhone = '555-0100'
appLabel = 'City Hall', -- sender name
message = 'You have been hired as a mechanic.', -- max 4096 characters
requireOnline = false, -- true = fail if the player is offline
push = { title = 'City Hall', text = 'You have a new message' }, -- false = no notification
})
if not result.ok then
print(result.error) -- e.g. TARGET_PHONE_NOT_FOUND
endTo read conversations (for example from a smartwatch), use GetMessageConversations(source | number | { source, limit, cursor }), GetThreadMessages({ source, threadId, limit, beforeMessageId }) and GetMessageUnreadCount(source). Each returns { ok = true, ... } and only shows threads the number belongs to.
System email to a player's Mail inbox. The player must have created a Mail account on the phone.
-- Short form
exports['qs-smartphone']:SendMail(source, 'Parking fine', 'You have been fined $500.', 'City Hall')
exports['qs-smartphone']:SendMail('[email protected]', 'Welcome', 'Thanks for joining the server.')
-- Full form
local result = exports['qs-smartphone']:SendMail({
targetSource = source, -- or to = '[email protected]' / { ... }, or targetIdentifier
subject = 'Service notice', -- max 500 characters
body = 'Your impound fee is due.', -- max 60000 characters
fromName = 'LSPD',
fromEmail = '[email protected]',
push = true, -- false = no notification
})
-- Does the player have a Mail account?
local account = exports['qs-smartphone']:GetMailAccount(source) -- nil, or { id, email, displayName, ownerIdentifier }Errors: MAIL_ACCOUNT_NOT_FOUND, INVALID_RECIPIENTS, INVALID_BODY, BODY_TOO_LONG, SUBJECT_TOO_LONG, SEND_FAILED.
-- Built-in billing only; with another billing script use its own API
local billId, err = exports['qs-smartphone']:CreateBill(targetSource, {
title = 'EMS',
subtitle = 'Medical treatment',
price = 500,
job = 'ambulance', -- uses Config.BillingJobs.ambulance as the icon
avatar = 'f7:cross_case_fill', -- optional
})
local bills = exports['qs-smartphone']:GetBills(source) -- { { id, title, subtitle, price, avatar, job }, ... }
local ok = exports['qs-smartphone']:PayBill(source, billId)Control the Marketplace "Enter service" state from your job script. Use framework job names; the phone finds the shop in config/marketplace.lua.
Export | Returns |
|---|---|
| Whether a shop uses that job. |
| Whether the player is on duty. |
| The job they are on duty as, or nil. |
| Clock in for that job, or out with no job. |
| Whether anyone is on duty for that job. |
For inventories that call an export when an item is used (ox_inventory, qs-inventory): qs-smartphone.useItem (phones), qs-smartphone.useSimCard, qs-smartphone.usePowerbank and qs-smartphone.useWirelessEarbuds. See Installation.
Run your own code before a call rings: build business lines, phone menus, call logs or job dispatch. The built-in service numbers (911…) use this same system. A complete example is in [examples]/phone-business-line.
exports['qs-smartphone']:registerCallInterceptor('555-1234', function(callData)
-- callData: sessionId, callerSource, callerNumber, callerDisplayName,
-- calleeSource, calleeNumber, calleeDisplayName, callType
return { action = 'allow' } -- ring normally
-- return { action = 'reject' } -- reject silently
-- return { action = 'handle' } -- hold the call; you accept or reject it within 30 s
-- return { action = 'reroute', reroute_target = playerId }
-- return { action = 'reroute', reroute_targets = { id1, id2 }, reroute_label = 'Mechanic' }
end, { priority = 10 })
-- '*' sees every call (runs after the specific numbers)
exports['qs-smartphone']:registerCallInterceptor('*', function(callData)
print(('[CALL] %s -> %s'):format(callData.callerNumber, callData.calleeNumber))
return { action = 'allow' }
end, { priority = -100 })Export | What it does |
|---|---|
| Connects a held call. |
| Rejects it. |
| Lets it ring the original number. |
| The held call's data. |
| Removes your interceptor. Also done when your resource stops. |
| Check what is registered. |
| Whether the player is in a call. |
| The current call: |
| Hangs up the player's call. |
Listen with AddEventHandler. These are local events; do not register them with RegisterNetEvent.
Event | Side | When |
|---|---|---|
| Client, server | The phone opened. Client: |
| Client, server | The phone closed. |
| Client | A call arrives, or its state changes. |
| Client | The player used the phone item. |
| Client | The player switched to another phone item. |
| Client | A button on your Dynamic Island was pressed. |
| Server | Marketplace duty changed: |
| Server | The steps of an intercepted call. |
-- Hide your HUD while the phone is open
AddEventHandler('phone:opened', function(payload)
if payload.mode == 'full' then
-- hide HUD
end
end)
AddEventHandler('phone:closed', function(payload)
-- show HUD
end)For a one-off check,
exports['qs-smartphone']:IsPhoneOpen()is simpler than tracking the events.