Pulse Phone docs
Integrations: the scripts Pulse Phone works with
Pulse Phone connects to the scripts your server already runs. Drop it in, start it after your framework and scripts, and it finds them on its own. There is nothing to set for a normal server.
This page lists every script the phone supports in each category, what "auto" picks when several are running, how to force one, and how to connect a script that isn't listed.
- The startup line. When the phone starts it prints one line naming what it connected to:
Phone Framework: QBCore | Database: oxmysql | Voice: pma-voice | Housing: qb-houses | Garage: qb-garages | Banking: framework | Company accounts: Renewed-Banking | Inventory: ox_inventory | Vehicle keys: qbx_vehiclekeys | Fuel: ox_fuel | Dispatch: ps-dispatch ... phone:diag(server console) lists every candidate for every category, which one is in use, and why each of the others was skipped.- A script it knows but can't use. If a supported script is running but isn't a version the phone recognises (a renamed or missing export), you get one yellow line naming the script and what is missing. The phone then uses the next option instead.
- Restarts. A script that starts or restarts after the phone is picked up within about two seconds.
Choosing or forcing a script
Every category has an entry in Config.Providers in config/config.lua:
Config.Providers = {
garage = 'auto', -- pick the best one that is running (the default)
keys = 'wasabi_carlock', -- always use this one (the resource name from server.cfg)
dispatch = 'none', -- switch the category off
...
}'auto'picks the highest-ranked supported script that is running. Each table below lists scripts from highest to lowest rank.- Two scripts of the same rank. If both are running, the one whose name comes first alphabetically is used. Name the one you want to be sure.
- A resource name always uses that script. If it isn't running,
phone:diagsays so and the category has no provider until it starts. 'none'switches the category off.
Most categories are optional. A server without a housing script simply has no homes in the Home app.
Framework
| Script | Notes |
|---|---|
ESX Legacy (es_extended) |
|
QBCore (qb-core) |
|
Qbox (qbx_core) |
Qbox also runs a qb-core bridge. The phone treats the two as one framework. |
| ox_core | |
vRP 2 (vrp) |
|
| Standalone | Used when no framework is running. The phone keeps its own bank ledger, jobs and test garage. |
| Anything else | Use Config.Framework = 'custom' with a bridge table shaped like developers/custom_framework_bridge.lua. |
Config.Framework in config/config.lua, or setr phone_framework <name> in server.cfg, forces
one. You must set one of them when more than one framework resource is started. The phone
refuses to guess, and says so in red.
Banking
Your own balance (Wallet, paying in Messages, bills, valet fees) is your framework's bank money. Every banking script below reads and writes that same money, so the phone and your bank always show the same number.
What a banking script adds:
| Script | Company accounts (Services) | Bank history in Wallet | Shared accounts in Wallet |
|---|---|---|---|
| wasabi_banking | yes | not public | no |
| tgiann-bank | yes (job accounts) | yes | no |
| Renewed-Banking | yes | yes | yes |
| tgg-banking | yes | not public | no |
| qs-banking | yes | not public | no |
| okokBanking | yes | yes (V2) | no |
| fd_banking | yes | not public | no |
| snipe-banking | yes | not public | no |
| qb-banking | yes | yes | yes |
| esx_banking | ESX company money is esx_addonaccount's | yes | no |
| qb-management (copies from before late 2023 that still hold money) | yes | no | no |
| ps-banking (archived) | no company accounts | yes | no |
| esx_addonaccount / esx_society | yes (society_<job>) |
no | no |
- Rank for company accounts: wasabi_banking, then tgiann-bank, then the scripts ranked 30 (Renewed-Banking, okokBanking, qs-banking, fd_banking, snipe-banking, tgg-banking), then qb-banking, qb-management, esx_addonaccount.
- Why wasabi_banking ranks first: it can answer calls meant for Renewed-Banking, qb-management and okokBanking. When it runs, its own exports are the ones the phone uses.
- Money safety:
- A company account never goes below zero from the phone.
- A move the banking script accepted, but whose balance never showed the change, is recorded as unconfirmed for staff to check. It is never refunded or retried automatically, because that could pay twice.
- Settings:
Config.Providers.society(company accounts),.statement(history) and.accounts(shared accounts).
"Not public" means the script publishes no way for another script to read its history. The Wallet then shows only the phone's own transfers.
Society, boss menus and job management
The phone's company features (Services > company account, deposits and withdrawals by the boss) use the job from your framework and the company account from the banking table above.
| Script | What happens |
|---|---|
| qbx_management | Holds no money. The company account comes from your banking script (usually Renewed-Banking). |
| qb-management | Current versions hold no money (qb-banking does). Older copies are supported directly. |
| esx_society | The account name comes from GetSociety. The money lives in esx_addonaccount. |
| okokBossMenu | Keeps money in the system its Config.SocietySystem names (addon-account, okokBanking, qb-banking or qb-management). Each of those is supported above. |
Vehicle keys
When the valet brings a car (Garage > valet), the player gets its keys through your key
script. Setting: Config.Providers.keys.
| Script | Rank | How the phone gives keys |
|---|---|---|
| qbx_vehiclekeys | 25 | server GiveKeys(source, vehicle) |
| Renewed-Vehiclekeys | 20 | server addKey(source, plate) |
| wasabi_carlock | 20 | server GiveKey(source, plate) |
| MrNewbVehicleKeys | 20 | server GiveKeys(source, netId) |
| vehicles_keys (jaksam) | 20 | server giveVehicleKeysToPlayerId(source, plate, 'owned') |
| ak47_vehiclekeys / ak47_qb_vehiclekeys | 20 | server GiveKey(source, plate, false) |
| qb-vehiclekeys | 20 | server GiveKeys(source, plate); before 1.3, its vehiclekeys:client:SetOwner event |
| qs-vehiclekeys | 20 | client GiveKeys(plate, model, false), on the player's own client |
| cd_garage (its built-in keys) | 15 | cd_garage:AddKeys to the player |
| okokGarage (its built-in keys) | 15 | the player's client raises okokGarage:GiveKeys |
| t1ger_keys | 20 | server UpdateKeysToDatabase(plate, true): the keys are recorded on the owned car, which is the one the valet brings |
| No key script | 0 | Nothing to hand over. The car is driveable as it is. |
- qbx_vehiclekeys and the qb-vehiclekeys name: qbx_vehiclekeys also answers to the name
qb-vehiclekeys. The phone uses qbx's own export, so both are never called. - A dedicated key script wins over a garage script's own keys, for example wasabi_carlock over cd_garage.
Remote lock and unlock (Garage app)
Setting: Config.Providers.vehiclelock.
| Script | How |
|---|---|
| qbx_vehiclekeys | SetLockState(vehicle, 'lock' \| 'unlock'). Vehicles it marks as having no lock are refused. |
| Renewed-Vehiclekeys | its vehicleLock state bag |
| MrNewbVehicleKeys | SetVehicleLock(netId, 1 \| 2) |
| t1ger_keys | The game's own door lock, plus its client SetVehicleLocked(vehicle, 2 \| 1) on the owner's client (t1ger keeps its lock in a decor) when the car is near them |
| qb-vehiclekeys, wasabi_carlock, cd_garage, vehicles_keys, okokGarage, qs-vehiclekeys, ak47 keys | The game's own door lock, which is what their key fobs toggle. None of them publishes a server call to set a lock (cd_garage's and qs-vehiclekeys' lock calls are client-side). |
| No key script | The game's own door lock |
Fuel (valet delivery)
A car the valet brings arrives with the fuel level the garage stored, not a full tank or a
random one. Setting: Config.Providers.fuel.
| Script | How |
|---|---|
| ox_fuel | the vehicle's fuel state bag, which ox_fuel reads when a driver gets in |
| Renewed-Fuel | its server SetFuel(vehicle, level) export, and the fuel state bag it reads |
| lc_fuel | its client SetFuel(vehicle, level) |
| qs-fuelstations | its client SetFuel(vehicle, level) |
| cdn-fuel | its client SetFuel(vehicle, level) |
| ps-fuel | its client SetFuel(vehicle, level) |
| BigDaddy-Fuel | its client SetFuel(vehicle, level) |
| okokGasStation | its SetFuel(vehicle, level) |
| x-fuel | its SetFuel(vehicle, level) |
| LegacyFuel | its client SetFuel(vehicle, level). Without it, LegacyFuel gives a car a random tank. |
| No fuel script | the state bag and the game's own fuel level |
- Order: the list above is the order of preference.
- Scripts that register themselves as
LegacyFuel(lc_fuel does) are asked by their own name. - How the client copy works: for the scripts with a client call, the player's own client copies the level into the script. It only does this when the script shows a different level, so it never fights a script that manages the state bag itself.
Garages (Garage app and valet)
The Garage app lists the character's vehicles, where each one is (parked at which garage, out
in the world, impounded at which lot and for how much), and offers lock/unlock and valet.
Setting: Config.Providers.garage.
| Script | Rank | Parked / impounded | Garage places | Valet |
|---|---|---|---|---|
| jg-advancedgarages | 30 | its in_garage, garage_id, impound and impound_data columns |
getAllGarages or its config |
off |
| cd_garage | 30 | its in_garage, garage_id and impound columns (from its former SQL page; the current docs no longer list them), plus GetVehicleImpoundData |
GetGarageLocations |
off |
| okokGarage | 30 | its parking and impoundTime columns (on QBCore, the garage column when parking is empty); the retrieve fee only when Config.RetrieveFeeEnabled is on |
its config.lua | on |
| vms_garagesv2 | 30 | its garage, impound_date and impound_data columns (impounded when both impound columns are set) |
getGarageInfo |
off |
| qs-advancedgarages | 30 | its garage column ('OUT' means out) and player_garages.isImpound |
player_garages |
off |
| qbx_garages | 21 | state, garage and depotprice |
GetGarages |
on |
| qb-garages | 20 | state, garage and depotprice |
getAllGarages or its config |
on |
| esx_garage | 20 | stored, parking and pound |
getGarages / getImpounds or its config |
on |
| esx_advancedgarage | 20 | stored (older and v1.0.0 configs, including its job pounds) |
its config | on |
| codem-garage | 15 | the framework's list, plus the garage name from its parking column (Impound Garage means impounded) |
not public | off |
| rcore_garage | 15 | the framework's list only (its storage is not public) | not public | off |
| loaf_garage | 15 | the framework's list only (its storage is not public) | not public | off |
| no garage script | 5 | the framework's own vehicle table | Config.Garage.garages |
on |
- Why valet is off for some scripts: those scripts record "parked" in a column of their own that the valet can't update. A delivered car would still show as parked in that script's menu, which would let the player take out a second copy. So the valet is switched off for them. The rest of the Garage app works as normal.
- Garages whose places aren't public: list them yourself in
Config.Garage.garages, and the app can point players to them.
Housing (Home app)
Setting: Config.Providers.housing. Each script supports only what it publishes:
| Script | List homes | Lock / unlock | Key holders | Give / take keys |
|---|---|---|---|---|
| qb-houses (+ qb-apartments) | yes | yes | yes | yes |
| ps-housing | yes | no (shells have no lock state) | yes | yes |
| qbx_properties | yes | no | yes | no |
| qs-housing | yes | not public | no | no |
| bcs_housing | yes | yes | yes | take only |
| nolag_properties | yes | yes | yes | yes |
| vms_housing | yes | no | yes | yes |
| rtx_housing | yes | yes (its server SetPropertyLockStatus, after its own unlocking permission check) |
yes | yes (through the events its own menu raises; not in its public docs) |
| loaf_housing | yes | yes (with loaf_keysystem) | yes | yes |
| esx_property | yes | no (its own menu only) | yes | no |
| qb-apartments on its own | yes | no | no | no |
- Ranking: every housing script ranks 6. qb-apartments ranks 5, so on its own it is used only when no other housing script runs.
- qb-houses with qb-apartments: the apartment appears beside the houses. House actions are never sent for the apartment.
Inventory
Used for the phone item, the power bank and add-on apps. Setting: Config.Providers.inventory.
| Script | Rank |
|---|---|
| qs-inventory, codem-inventory, core_inventory, tgiann-inventory, origen_inventory, ak47_inventory | 25 |
| ox_inventory | 20 |
| qb-inventory (old and new), ps-inventory, lj-inventory, the ESX inventory, vRP | 10 (through the framework's own item functions) |
Scripts that register themselves under another inventory's name (tgiann-inventory and
origen_inventory can register as qb-inventory or ox_inventory) rank above that name, so
the right one is asked.
Dispatch (Smart 911, Services)
Setting: Config.Providers.dispatch.
| Script | How |
|---|---|
| ps-dispatch 3.x | SendTargetedAlert to the on-duty responders |
| ps-dispatch 2.x | its ps-dispatch:server:notify event. Its clients filter by job and duty. |
| cd_dispatch | cd_dispatch:AddNotification |
| core_dispatch | addCall |
| qs-dispatch | qs-dispatch:server:CreateDispatchCall |
| rcore_dispatch | rcore_dispatch:server:sendAlert |
| linden_outlawalert | wf-alerts:svNotify |
| No dispatch script | a phone notification to every online player with the job |
ps-dispatch 1.x (dispatch:server:notify) is not supported.
Billing and invoices (Wallet > Bills)
Bills from these scripts show up in Wallet > Bills and can be paid there. Paying a bill settles it in the billing script's own records.
| Script | How |
|---|---|
| The phone's own bills | the SendBill export (developers/EXPORTS.md) |
| esx_billing | its billing table |
| okokBilling | its okokbilling table (unpaid / paid) |
QBCore phone_invoices |
the table QBCore's phone schema uses (with its reason, shown as the bill's reason) |
A bill with nobody to receive the money is refused before anything is taken, unless you turn
on Config.Bills.payToCity.
Pulse Tablet: medical scripts and billing
The EMS tablet's body scan reads the injuries the server's medical script keeps
(server/custom/functions/medical.lua; pin one with Config.Providers.medical):
| Script | What is read |
|---|---|
| qbx_medical | the qbx_medical:injuries:<PART>, qbx_medical:bleedLevel and qbx_medical:deathState state bags |
| ars_ambulancejob | the injuries and dead state bags |
| wasabi_ambulance | v2: getPlayerInjuryTotals (kinds of injury, no body map) and wasabi:deathState; v1: dead |
| qb-ambulancejob | the limbs its own client syncs (hospital:server:SyncInjuries); death from metadata |
| esx_ambulancejob | the isDead state bag only |
With none of them, the tablet places the injury on the bone the scanning client last saw hit
(GetPedLastDamageBone) and sizes it by the health the server sees lost; it says so on screen.
A printed ticket (and, with Config.Tablet.ems.reportFee, a medical report) is billed once,
through the first that runs (server/custom/functions/billing.lua):
| Script | How |
|---|---|
| okokBilling | okokBilling:CreateCustomInvoice, sent from the officer's own client as its docs show |
| esx_billing | exports.esx_billing:BillPlayer(target, officer, 'society_<job>', label, amount) |
| wasabi_police (v2) | exports.wasabi_police_v2:FinePlayer(target, amount, reason) |
| jim-payments | a row in qb-phone's phone_invoices table, which jim-payments reads |
| (none) | the phone's own invoices (Wallet > Bills), else a direct bank charge with a statement line |
ps-mdt documents no server API for reports, warrants or fines, so nothing is pushed to it; its
IsCidFelon export is shown on a person. qb-policejob and qbx_police bill and jail only from an
officer's own client, so the tablet does not call them. Jail time on a ticket is recorded; with
Config.Tablet.citations.sendToJail = true it is sent through wasabi_police_v2:JailPlayer or
qbx_prison:JailPlayer when one runs.
Door locks (for add-on apps)
| Script | Read a door | Lock / unlock |
|---|---|---|
| ox_doorlock | yes | yes. The phone checks the door's groups, characters and items first. |
| qb-doorlock | no | yes, for a player. qb-doorlock checks access itself. |
| cd_doorlock | yes | yes, for a player |
| nui_doorlock | – | Not supported. It has no server API and is archived. |
Voice, target and notifications
- Voice (calls): pma-voice, mumble-voip, SaltyChat, TokoVOIP, YACA.
- Target (payphones, the phone store clerk): ox_target or qb-target when one runs. Otherwise the phone shows an [E] prompt.
- Notifications: the phone shows its own notifications on the handset and does not need ox_lib, okokNotify, mythic_notify or your framework's notify. Scripts that send notifications to a phone reach it through the events in developers/EXPORTS.md.
Connecting a script that isn't listed
Every category is a provider: a table of functions the phone calls. To connect an in-house or unlisted script, register your own provider from your own resource. Pulse Phone picks it up at once and drops it when your resource stops:
-- in your resource (server side)
exports.phone:RegisterProvider('keys', 'my_keys', {
Available = function() return GetResourceState('my_keys') == 'started' end,
Describe = function() return 'my_keys' end,
GiveKeys = function(src, plate, vehicle)
exports.my_keys:GiveKey(src, plate)
return true, 'my_keys'
end,
}, 50) -- rank: above the built-in ones (at most 40), so it wins in 'auto'Or pin it: Config.Providers.keys = 'my_keys'.
Rules for a provider:
- Never throw. For anything you can't answer, return
nil, reasonorfalse, reason(or callcb(nil, reason)). - A function that takes
cbmust always call it exactly once. Available()is optional. Returnfalse, reasonwhile your script isn't ready.
developers/custom_provider_template.lua has complete housing, garage, dispatch and weather
providers. The phone's own adapters in bridge/server/ show how every supported
script is connected.
The functions each category needs
| Category | Functions (? = optional) |
|---|---|
keys |
GiveKeys(src, plate, vehicle) -> ok, via |
vehiclelock |
SetLocked(src, vehicle, plate, locked) -> ok, via, IsLocked?(vehicle) |
fuel |
SetFuel(vehicle, level) -> ok, via (server), Client?() -> client adapter name |
garage |
ListVehicles(src, cb(list)), GetGarageLocation?(name); set ValetUnsafe = true if your script keeps "parked" in its own column. Row shape: developers/CUSTOM_GARAGE.md |
housing |
ListOwned(src, cb(list)), GetLocation?(id), SetLocked?(src, id, locked, cb), ListKeyholders?(src, id, cb), GrantAccess?(src, id, charId, cb), RevokeAccess?(src, id, holder, cb) |
society |
GetBalance(job), AddMoney(job, amount, reason), RemoveMoney(job, amount, reason). Money moves answer true, false (nothing moved) or 'unconfirmed'. |
statement |
GetStatement(src, limit, cb(rows)). Each row is { id, kind = 'sent' \| 'received', amount, party_name, note, at (ms) }. |
inventory |
HasItem, GetItemCount, AddItem, RemoveItem, GetInventory, RegisterUsable? |
dispatch |
Send(alert) -> count. alert = { title, body, code, jobs, coords, priority, caller } |
doorlock |
GetDoor?(ref), CanOpen?(src, ref), SetLocked(ref, locked, src?) |
Client-side key, lock and fuel scripts are listed in bridge/client/vehicle.lua.
Add a line to its KEYS, LOCKS or FUEL table for your script, then return that name from
your server provider (Client() for fuel; for keys, use the phone:keys:give relay as
qs-vehiclekeys does; for locks, the phone:vehlock:set relay as t1ger_keys does).
A whole framework the phone has no adapter for: developers/custom_framework_bridge.lua, with
Config.Framework = 'custom'.
GetCharacterIdis required.- Leave out a name, job, licence or vehicle/home/business list function and the phone answers as a server with no such data would.
- Leave out a money or item function and that action is refused (
false, 'not-implemented'). The phone never pays from a bank of its own beside your framework's. - The Bank app appears only when the bridge has
GetBankBalance. - If your core restarts while the phone keeps running, call
exports.phone:FrameworkRestarted()once the core is ready, so the phone registers its usable items again.
Not supported, and why
- nui_doorlock: no server API, and the repository is archived.
- ps-dispatch 1.x: replaced by 2.x and 3.x, which are supported.
- rcore_garage, loaf_garage: their storage isn't public. The Garage app lists vehicles from your framework's table and keeps valet off.
- Chezza's inventory: no public documentation. On ESX it still works through ESX's own item functions.
- Bank history for qs-banking, fd_banking, snipe-banking, tgg-banking and wasabi_banking: they publish no way to read it.
- wasabi_banking shared and savings accounts: it documents
GetPlayerAccounts, but with no account names or member roles, so the phone can't tell whether a member may take money out. They are left out of Wallet rather than let every member withdraw.
Every adapter is built only from each script's public documentation or public source. Where a
detail isn't public, the phone doesn't guess: it skips that feature for that script and says
so here and in phone:diag.
Video relay configuration (advanced)
Pulse supports direct WebRTC connectivity with TURN relay support for reliable cross-network video calling. For production servers, configure a TURN service before launch. Video uses the in-game camera; call sound uses the selected voice script. Players do not need a physical webcam.
- Obtain a TURN endpoint and credentials from your relay service, or configure coturn.
- Put the settings in a private configuration file beside server.cfg, outside the
phoneresource. Addexec pulse-services.cfgbeforeensure phone. - Use
setfor credentials, neversetrorsets. Do not put tokens or relay secrets inconfig/config.lua, a client script, NUI, a public repository or a support message. - Restart Pulse and run
phone:healthin the server console. Correct any configuration errors.
# pulse-services.cfg — private, server-side
set phone_turn_urls "turn:<your-relay-host>:3478?transport=udp,turns:<your-relay-host>:5349?transport=tcp"
set phone_turn_username "<your-relay-username>"
set phone_turn_credential "<your-relay-password>"Use the exact endpoints supplied by your service. A TURN login is different from a provider's management API token. Pulse accepts standard TURN credentials or coturn shared-secret credentials; services offering only a credential-generation API need a server-side integration for that API.
The STUN servers are in Config.WebRTC.iceServers in config/config.lua. The default entries
help direct connections discover a network route. With TURN configured, Pulse uses the relay for
video and filters direct network addresses from its signaling. The relay therefore needs enough
bandwidth for your video calls and live streams. connectTimeout controls the picture connection
window, and interrupted connections use bounded ICE retries.
Short-lived credentials (coturn). With coturn's use-auth-secret / static-auth-secret,
give the phone the secret instead of a fixed login. Every call then gets its own credentials that
expire, so a player who copies them out of a call can't keep using your relay:
set phone_turn_urls "turn:<your-turn-host>:3478"
set phone_turn_secret "<the static-auth-secret from turnserver.conf>"
set phone_turn_ttl 14400 # seconds the credentials stay valid (60 to 86400; 4 hours by default)With phone_turn_secret set, phone_turn_username and phone_turn_credential are not used.
The shared secret stays on the server. Only expiring session credentials are sent to participants.
Keep the TTL longer than your longest expected call; keep the server and relay clocks synchronized.