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:

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:diag says 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:

Lua
-- 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, reason or false, reason (or call cb(nil, reason)).
  • A function that takes cb must always call it exactly once.
  • Available() is optional. Return false, reason while 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'.

  • GetCharacterId is 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.

  1. Obtain a TURN endpoint and credentials from your relay service, or configure coturn.
  2. Put the settings in a private configuration file beside server.cfg, outside the phone resource. Add exec pulse-services.cfg before ensure phone.
  3. Use set for credentials, never setr or sets. Do not put tokens or relay secrets in config/config.lua, a client script, NUI, a public repository or a support message.
  4. Restart Pulse and run phone:health in the server console. Correct any configuration errors.
server.cfg
# 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:

server.cfg
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.