Docs

X3D Phone

Free ESX smartphone: pma-voice calls, SMS with location sharing, mobile banking, a camera with self-hosted photos, Buzz and anonymous Dark Chat.

Buy / Get freeConfig builder

Installation

  1. Extract the zip and put the x3d_phone folder in resources/. Do not rename the folder: every export and event uses this name.
  2. Run install.sql on your server database. It is safe to run again and keeps existing data.
  3. If your server has no phone item, run optional/phone_item.sql. With ox_inventory, add the item to ox_inventory/data/items.lua instead; the file has an example.
  4. If your server has no screenshot-basic, move optional/screenshot-basic/ into resources/. You can skip this; only the camera needs it.
  5. Add these lines to server.cfg, in this order:
ensure oxmysql
ensure es_extended
ensure pma-voice
ensure screenshot-basic
ensure x3d_phone

Players open the phone with F1, the /openphone command, or by using the phone item. While the phone is open, Alt switches to game controls; press it again to get the cursor back.

Requirements

Resource Required Used for
es_extended no ESX Legacy. Without it the script runs standalone and you wire banking and billing yourself in addons/ (see Config.Framework)
oxmysql yes database
pma-voice no call audio; without it calls connect but carry no audio
screenshot-basic no the camera; a copy is bundled in optional/
chat (FiveM) no post cards in chat

No image host, no API key, and no other software to install.


All settings: config/config.lua

General

Setting Default Meaning
Config.Locale 'th' Language of the phone and its messages: 'th' or 'en'
Config.Framework 'auto' 'auto' (ESX if present, else standalone) | 'esx' | 'standalone'
Config.Debug false true prints logs to the server console
Config.OpenKey 'F1' Key that opens the phone. Players can rebind it in Settings > Key Bindings > FiveM. This value only applies to a player's first join.
Config.DefaultContacts {} Contacts added to every new number, e.g. { phone = '911', name = 'Police' }

Phone item

Setting Default Meaning
Config.UseItem true The item must be in the inventory to open the phone, call, answer and get SMS alerts
Config.PhoneItem 'phone' Item name. You can use one your server already has, e.g. 'iphone'

With UseItem on:

  • No item: the phone does not open, and every server callback refuses. Triggering events by hand does not get past this.
  • A player without the item cannot be called. The caller sees "unavailable" and the call is logged as missed.
  • SMS are still stored, but no alert shows until the player has the item.
  • If the item leaves the inventory while the phone is open (dropped or taken), the phone closes and any call ends.

Calls

Setting Default Meaning
Config.UseVoice true Use pma-voice for call audio
Config.CallChannelStart 1000 First pma-voice channel used for calls. Change it only if it collides with another script.
Config.CallTimeout 30 Seconds before an unanswered call ends
Config.Ringtones 5 sounds Ringtones; each maps to a GTA V sound
Config.NotificationSounds 3 sounds Message alert sounds
Config.RingtonePreviewMs 3000 Length of /testringtone and /testdialtone

Config.Ringtones, Config.NotificationSounds and Config.Wallpapers are what the server checks a player's choice against. You can remove an option in the config. Adding a new option also needs a change to the phone's web UI.

Messages

Emoji work in every text box (SMS, Buzz, Dark Chat), with a picker holding all 3,600+ emoji in 9 groups plus search. Lengths are counted in real characters, so Thai text and emoji are not over-counted.

Setting Default Meaning
Config.MaxMessageLength 500 Maximum length of an SMS or a Dark Chat message

Buzz and post card skins

Setting Default Meaning
Config.Buzz.ToChat true New posts appear in everyone's in-game chat
Config.Buzz.Themes all 5 Skins on offer: default, halloween, christmas, songkran, night
Config.Buzz.DefaultTheme 'default' Skin for players who have not picked one
Config.Buzz.PlayerTheme true true: players pick their skin in Settings. false: everyone uses DefaultTheme.

Example: lock the whole server to Halloween for the event:

Config.Buzz = {
    ToChat = true,
    Themes = { 'halloween' },
    DefaultTheme = 'halloween',
    PlayerTheme = false,
}

Post cards use FiveM's standard chat resource. x3d_phone adds its own styles through chat_theme and leaves your chat theme alone. Post text is always escaped, so players cannot inject HTML into chat.

Service numbers (police / EMS / mechanic) and job scripts

Setting Default Meaning
Config.Dispatch.Numbers police, ambulance, mechanic Service numbers: number = { label = 'Shown name', jobs = { 'job' }, coords = true }
Config.Dispatch.OnDutyOnly false true delivers only to players on duty (ESX Legacy job.onDuty)
  • Service numbers appear as special contacts for everyone and cannot be edited or deleted.
  • Texting police delivers [Police] ... to every online player with a job in jobs who has a phone. It arrives from the sender's own number with their location, so a tap sets a GPS waypoint and a reply goes straight back to the sender.
  • The location comes from the character's position on the server, so it cannot be faked. One message per service every 20 seconds.
  • If nobody from the service is online, the sender is told.

Hooking up job scripts: the police, ambulance and mechanic numbers already ship in the config, and your job scripts can add or remove numbers with these events.

Event What it does
TriggerEvent('x3d_phone:registerNumber', number, label, isDispatch, hasCoords) (server) Adds a service number from a job script (an existing number is left alone)
TriggerEvent('x3d_phone:removeNumber', number) (server) Removes a registered number (numbers in the config stay)
TriggerServerEvent('x3d_phone:send', number, message, anonymous) Scripts that alert the police automatically (robberies etc.) send a message to a service number. With anonymous = true the message comes from the service number instead of the player's.

Job scripts that call the old esx_phone:* event names do not work with x3d_phone; point them at the events above.

Dispatch app

Responders (players with a job in jobs) get a Dispatch app. Each call shows a title, message, distance, age and the caller's number, plus a flashing map blip, a notification and a sound.

  • Respond sets a GPS waypoint, tells the caller "X accepted and is on the way", and shows the other responders that someone has it.
  • Close call removes it from every phone and map. Calls nobody closes expire after AlertSeconds.
  • Calls come from texts to a service number, or from other scripts requesting a service.
Setting Default Meaning
Config.Dispatch.AlertSeconds 600 How long a call stays in the app
Config.Dispatch.CallNeedsPhone false Whether the caller needs the phone item (a "call EMS" button while downed should not)
Numbers.<number>.request Call title when someone requests the service, e.g. 'Person injured, EMS needed'
Numbers.<number>.blip { sprite, color } of the responder's map blip

Requesting a service from other scripts (e.g. a "call EMS" button on the death screen):

-- client
exports['x3d_phone']:RequestService('ambulance', 'Unconscious in the street')
-- or
TriggerServerEvent('x3d_phone:requestService', 'ambulance', 'Unconscious in the street')

-- server (set your own title / coords)
exports['x3d_phone']:SendAlert('police', { source = src, title = 'Bank robbery', message = 'Pacific Standard' })
exports['x3d_phone']:SendAlert('police', { coords = vector3(x, y, z), title = 'Alarm triggered' })

RequestService reads the location from the caller's character on the server, so it cannot be faked, and a player can call the same service once every 60 seconds.

Bills app

Setting Default Meaning
Config.Billing.Enabled true Shows the Bills app
Config.Billing.AllowPersonal true Allows personal bills. The money goes to the biller's bank account, even when they are offline.
Config.Billing.AllowDecline true The recipient can decline. With false they can only pay (the biller can still cancel).
Config.Billing.MinAmount / MaxAmount 1 / 1000000 Smallest / largest bill
Config.Billing.MaxOpenPerTarget 10 Most unpaid bills one biller can have open to one number (anti-spam)
Config.Billing.PayFrom 'bank' Which account the payer pays from: 'bank' or 'money'
Config.Billing.UsersTable 'users' ESX players table, used to credit a biller who is offline
Config.Billing.Jobs police, ambulance, mechanic, taxi Jobs that can bill on behalf of the business: jobname = { label = 'Shown name', minGrade = 0, locked = true } · locked = true = the biller cannot cancel and the recipient cannot decline; it is paid or an admin cancels it in x3d_adminpanel (default: police, ambulance)
Config.Billing.Admin { enabled = true, label = 'แอดมิน', groups = { 'admin', 'superadmin' }, locked = true } Admins can bill as "Admin" (e.g. fines); the money goes to Billing.PayAdmin at the end of addons/billing/server.lua, credited to nobody by default

Locked bills can only be cancelled in x3d_adminpanel → Phone → Unpaid bills (search by number or description, hold to cancel, with a cancel log).

Business bills: by default there is no society account (Billing.PaySociety returns false), so the payer is not charged and the bill stays unpaid. Wire your server's society account (for example esx_addonaccount) in addons/billing/server.lua before you use business bills.

  • The recipient gets an alert at once if online. If offline, the bill waits in the app.
  • A bill can be paid once; tapping repeatedly never charges twice.
  • One bill every 5 seconds per player, and descriptions go through FilterMessage like other messages.

Billing from other scripts (server):

local ok, idOrError = exports['x3d_phone']:CreateBill(source, '48213', 500, 'Parking fine', 'police')
-- issuer: 'personal', a job name in Config.Billing.Jobs (the biller must actually have that job) or 'admin' (the biller must be in Config.Billing.Admin.groups)
Setting Default Meaning
Config.Uploads.Enabled true Enables the camera and wallpaper uploads from the PC
Config.Uploads.BaseUrl '' Can stay empty; see below
Config.Uploads.MaxBytes 3 MB Maximum size per image (hard ceiling 8 MB)
Config.Uploads.PerPlayerPerHour 30 Uploads allowed per player per hour
Config.Uploads.Keep 1000 How many recent images to keep. After that the oldest is overwritten, and its link stops working.
Config.Uploads.Quality 0.85 JPG quality of camera photos (0–1)
Config.ImageHosts imgur, ibb External hosts players may paste image links from (chat, posts, wallpaper). {} turns external links off.
Config.AnyImageHost false true accepts https links from any site

Where images are stored: in x3d_phone/data/uploads/ on your server. They are served through FXServer's own game port (30120), with no outside service. Worst-case disk use is Keep × MaxBytes, about 3 GB by default, but a camera photo is normally under 400 KB.

BaseUrl: when empty, image links are stored as /i/<token>.jpg. Each player's client adds the address of the server it is connected to, so this works with no setup. Set BaseUrl only when:

  • you have an https domain pointing at the server (for example through a reverse proxy), and you want photos to show inside post cards in chat.
  • Example: BaseUrl = 'https://img.myserver.com/x3d_phone'

Why cdn.discordapp.com is not a default host: Discord file links expire after about 24 hours when opened outside Discord, so the image would be gone the next day.

⚠️ AnyImageHost = true: the image host sees the IP address of every player who views the image, and you do not control what the image shows. Prefer listing the hosts you trust in ImageHosts.

Mobile banking

Setting Default Meaning
Config.Banking.Enabled true Shows the Banking app
Config.Banking.MinTransfer 1 Smallest transfer
Config.Banking.MaxTransfer 1000000 Largest transfer

Transfers go to the phone number of an online player. They use the ESX bank account and are logged in the x3d_phone_bank_history table. A transfer cannot start while another one is still running, and a player can transfer once every 3 seconds.


Copies to Discord

Add these to server.cfg. The webhook URLs stay on the server; players never see them.

set x3d_phone_upload_webhook "https://discord.com/api/webhooks/..."
set x3d_phone_log_webhook    "https://discord.com/api/webhooks/..."
Convar What it sends
x3d_phone_upload_webhook A copy of every photo a player takes or uploads, with name, server id, identifier and type (messages / twitter / wallpaper)
x3d_phone_log_webhook Logs of SMS, posts, Dark Chat and transfers (optional)
  • Mentions are off in every post, so a player named @everyone pings nobody.
  • Posts are queued, one every 1.5 seconds, to stay under Discord's rate limit. When more than 20 are waiting, new ones are skipped.
  • The photo itself stays on your server, so in-game links never expire.
  • Only URLs that look like real Discord webhooks are accepted.

Fitting your server: addons/

Edit these files freely. Each one works out of the box.

File Functions Default
addons/notify/client.lua Notify(message, kind) esx_notify → ox_lib → ESX.ShowNotification
addons/voice/client.lua Voice.Enabled(), Voice.SetCallChannel(ch), Voice.SetMuted(bool) pma-voice (mumble-voip example included)
addons/upload/client.lua Upload.Screenshot(kind, cb), Upload.Target(kind, cb) screenshot-basic → stored on your server
addons/framework/server.lua / client.lua The framework bridge (Framework.*), identical across every X3D product ESX if present, else standalone
addons/billing/server.lua Billing.Charge, Billing.Refund, Billing.PaySociety, Billing.PayPerson, Billing.PayAdmin Charges / refunds and pays the biller's bank through Framework.*; PaySociety returns false, wire it yourself
addons/banking/server.lua Banking.GetBalances, Banking.Transfer, Banking.History ESX bank account
addons/filter/server.lua FilterMessage(src, kind, message) blocks words in Config.BannedWords
addons/log/server.lua PhoneLog(src, kind, data), UploadCopy(p, file) Discord webhooks from convars
addons/locale/shared.lua T(th, en) follows Config.Locale

Example: to use your own banking system, change Banking.Transfer to call it and return true, or false, 'error message'. The server has already checked the amount, the recipient and the rate limit.

Example: banned words:

Config.BannedWords = { 'badword1', 'badword2' }

Exports (client)

exports['x3d_phone']:OpenPhone()
exports['x3d_phone']:ClosePhone()
exports['x3d_phone']:IsPhoneOpen()     --> boolean
exports['x3d_phone']:GetPhoneNumber()  --> string | nil
exports['x3d_phone']:IsInCall()        --> boolean
exports['x3d_phone']:GetCallData()     --> { phoneNumber, displayName, isOutgoing } | nil

Commands

Command What it does
/openphone Open or close the phone
/testringtone Play your selected ringtone
/testnotification Play your selected alert sound
/testdialtone Play the outgoing-call tone

Troubleshooting

Symptom Cause / fix
F1 says "You don't have a phone" Config.UseItem = true and the item is not in the inventory. Give the phone item, or set UseItem = false.
No camera button screenshot-basic is not ensured, or Config.Uploads.Enabled = false
A photo fails with upload_failed Check the console. If it says cannot write data/uploads/, create the data/uploads/ folder in the resource.
Calls connect but carry no audio pma-voice is not installed, or is ensured after x3d_phone
Posts do not appear in chat Config.Buzz.ToChat = false, or the server does not run FiveM's chat resource
A pasted image link will not send The host is not in Config.ImageHosts, or the link does not start with https
View product page